Programación Orientada a Objetos: Principios SOLID

Sin categoría

Pablo Vallecillos

  Sin categoría

Introducción

SOLID es un acrónimo inventado por Robert C.Martin (también conocido como el Tío Bob) para establecer los cinco principios básicos de la programación orientada a objetos.

Estos principios establecen prácticas que se prestan al desarrollo de software con consideraciones para su fácil mantenimiento y expansión a medida que el proyecto se amplía. Adoptar estas prácticas nos puede ayudar a evitar los «code smells», refactorizar el código y aprender sobre el desarrollo ágil y adaptativo de software.

SOLID representa:

  • S: (Single) Principio de responsabilidad única
  • O: (Open) Principio abierto-cerrado
  • L: (Liskov) Principio de sustitución de Liskov
  • I: (Interface) Principio de segregación de interfaz
  • D: (Dependency) Principio de inversión de dependencia

En este artículo, intentaremos comprender cada uno de ellos con pequeños ejemplos en php, aunque pueden ser aplicados a cualquier lenguaje orientado a objetos:

Principio de responsabilidad única

Este principio nos indica que cada clase debería tener una sola responsabilidad, por lo tanto promueve la colaboración entre clases.

Por ejemplo, imaginemos que queremos implementar el registro de un nuevo usuario a nuestra aplicación:

class UserController
{
    function register(array $data)
    {
        if (isset($data['password'])) {
            $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
        }
        User::create($data);
    }
}

Podemos destacar que en la función register primero creamos un hash para la contraseña y después el usuario, pero si quisiéramos cumplir este principio deberíamos crear una clase para crear el hash.

class PasswordHasher
{
    static function hash($password): string
    {
        return password_hash($data['password'], PASSWORD_DEFAULT);
    }
}
class UserController
{
    function register(array $data):
    {
        if (isset($data['password'])) {
            PasswordHasher::hash($data['password']))
        }
        User::create($data);
    }
}

De esta forma hemos separado la lógica de encriptación de contraseña de la creación del usuario, así cada una tiene su función o responsabilidad facilitando su cambio, mantenimiento o extensión en un futuro.

Principio abierto-cerrado

Este nos indica que una entidad software debe estar abierta para su extensión pero cerrada para su modificación.

Para ello cuando añadamos nuevas funcionalidades a nuestra aplicación debemos escribir nuevo código, en lugar de modificar el código existente que seguramente ya funcione, tenemos que intentar escribir código que no se tenga que cambiar cada vez que tenemos que modificar los requerimientos.

Por ejemplo, si quisiéramos enviar distintas notificaciones por diferentes canales a nuestros usuarios:

class NotificationController
{
    function send(array $notifications)
    {
        foreach ($notifications as $notification) {
            $type = $notification['type'];
            if ($type == 'email') {
                $this->sendEmailNotification();
            } elseif ($type == 'telegram') {
                $this->sendTelegramNotification();
            }
        }
    }
}

Pero si en futuro necesitamos enviar por Slack, Whatsapp o sms tendriamos que modificar la función send, sin embargo si aplicamos dicho principio crearíamos la interfaz NotificationSender

interface NotificationSender
{
    function notify(array $data);
}

y los diferentes canales implementarían la interfaz

class EmailSender implements NotificationService
{
    function notify(array $data)
    {
        $this->send($data);
    }
}
class TelegramSender implements NotificationService
{
    function notify(array $data)
    {
        $this->send($data);
    }
}

De esta forma nuestro controlador quedaría abierto para nuevas modificaciones sin tener que modificarlo directamente, favoreciendo su mantenimiento y escalabilidad en el tiempo.

class NotificationController
{
    function send(array $notifications)
    {
        foreach ($notifications as $notification) {
            $notification->notify();
        }
    }
}

Principio de sustitución de Liskov

Este principio nos dice que toda clase que es hija de otra clase debe poder utilizarse como si fuera el mismo padre. Es decir una clase hija debería porder sustituir a su clase padre.

Por ejemplo imaginemos una clase pato:

class Duck
{
    function fly() {}
    function swim() {}
    function cuack() {}
}

En la que sus hijos heredarían de ella los métodos fly, swim y cuack. Pero si quisiermos crear una clase pato de goma heredando de la clase pato anterior no se cumpliría dicho principio ya que cuando usemos dichas clases tendremos que añadir una lógica adicional, por ejemplo:

class RubberDuck
{
    function fly() {
        throw new \Exception("can't fly");
    }
    function swim() {}
    function cuack() {}
}
class DuckProcesser
{
    function makeDucksFly(array $ducks) {
        foreach($ducks as $duck) {
            try {
                $duck->fly();
            } catch (\Exception $e) {
                throw $e;
            }
        }
    }
}

Para cumplir este principio podemos rediseñar las clases creando interfaces para cada acción de nuestro pato:

interface IFly
{
    function fly(): void;
}

interface ICuack
{
    function cuack(): void;
}

interface ISwim
{
    function swim(): void;
}

class RubberDuck implements ICuak, ISwim
{
    function swim() {}
    function cuack() {}
}

class Duck implements ICuak, ISwim, IFly
{
    function fly() {}
    function swim() {}
    function cuack() {}
}
class DuckProcesser
{
    function makeDucksFly(array $ducks) {
        foreach($ducks as $duck) {
            $duck->fly();
        }
    }
}

De esta forma cada tipo de pato implementa lo que puede hacer, este diseño es aún mas escalable en el tiempo y nos evitaría tener funciones que no hicieran nada como en el primer diseño propuesto que incumplía el principio.

Principio de segregación de interfaz

Este principio trata sobre la división en interfaces de una clase. Divide y vencerás. Nos dice que varias interfaces funcionan mejor que una sola.

Por ejemplo tendríamos un código más mantenible y robusto si en lugar de implementar toda la funcionalidad de nuestra lógica en una sola clase la implementásemos en varías interfaces.

Supongamos un sistema de impresión en que la clase Job se encargará de imprimir, grapar las hojas, enviar fax…

class Job
{
    function print() {}
    function fax() {}
    function staple() {}
    ...
}

El problema de este diseño se presentaría cada vez que quisieramos añadir nuevas funcionalidades o refactorizar las existentes de nuestro sistema. Cada cambio se volvería mas costoso que el anterior, sin embargo si aplicamos este principio podemos mejorar el diseño creando clases que especializan cada tipo de trabajo:

class PrintJob extend Job
{
    function print() {}
}
class StapleJob extend Job
{
    function staple() {}
}
class FaxJob extend Job
{
    function fax() {}
}
private class Job
{
    ...
}

De esta forma quedan definidas mucho mejor las necesidades, y el sistema tiene mayor escalabilidad. No se podría utilizar la clase Job, pero sí las clases que extienden de ella, definidas para desempeñar funciones específicas.

Principio de inversión de dependencia

Este principio se basa en la abstracción. Nos dice que los módulos de alto nivel no deberían depender de los módulos de bajo nivel. Este principio permite el desacoplamiento.

Es decir las implementaciónes concretas no deberían depender de otras implementaciones concretas si no de capas abstractas. Veámoslo en el siguiente ejemplo:

class MySQLConnection
{
    public function connect()
    {
        // handle the database connection
        return 'Database connection';
    }
}

class PasswordReminder
{
    private $dbConnection;

    public function __construct(MySQLConnection $dbConnection)
    {
        $this->dbConnection = $dbConnection;
    }
}

Vemos que la clase PasswordReminder (alto nivel) depende directamente de MySQLConnection (bajo nivel), pero si en futuro quisieramos cambiar nuestro gestor de base de datos este diseño no sería optimo, pero si aplicamos el principio sí:

interface DBConnectionInterface
{
    public function connect();
}

class MySQLConnection implements DBConnectionInterface
{
    public function connect()
    {
        // handle the database connection
        return 'Database connection';
    }
}

class PasswordReminder
{
    private $dbConnection;

    public function __construct(DBConnectionInterface $dbConnection)
    {
        $this->dbConnection = $dbConnection;
    }
}

De esta forma nuestro sistema depende de una abstracción lo que nos ofrece poder cambiar facilmente nuestro gestor de base de datos.

Conclusión

Los proyectos que se adhieren a los principios SOLID pueden compartirse con los colaboradores, ampliarse, modificarse, probarse y refactorizarse con menos complicaciones.

Referencias