Newsletter para devsEntra

Manual de buenas prácticas: O de S.O.L.I.D. Abierto / Cerrado

Seguimos con la saga de buenas prácticas y con otro de los principios S.O.L.I.D. basados en las enseñanzas del tío BOB (Robert C. Martin - Clean Code).

Si en capítulos anteriores de esta saga veíamos el principio de responsabilidad única, hoy nos vamos a centrar en la letra O.

Os dejo enlaces al resto de episodios de la serie por si queréis navegar por estos principios:

La O hace referencia a Open/Closed principle (principio Abierto/Cerrado) y nos viene a referir que un objeto o entidad dentro de nuestro código debe estar abierto a la extensión pero cerrado a la modificación.

Este concepto que de primeras nos puede resultar un poco confuso, viene a dar solución al problema que nos encontramos cuando nuestra aplicación adquiere una extensión considerable y tenemos que realizar cambios sobre el código ya escrito. Cuando realizamos una refactorización o ampliación.

Volver sobre el código ya escrito para realizar una modificación o ampliación, puede resultar a veces peligroso ya que simplemente podemos pasar por alto modificaciones que son importantes para poder lograr la lógica que la aplicación tenía en un principio. Caso que se magnifica si tenemos en cuenta que pueden pasar meses hasta que tengamos que volver a un código ya escrito para su modificación.

El principio Open/Closed de S.O.L.I.D. mal aplicado

Imaginemos un caso…

Tenemos una clase que se encarga de realizar un LOG de una acción en nuestra aplicación. Vamos a imaginar un LOG que en este caso vuelca a un fichero de texto una línea que informa que un usuario ha hecho login en la aplicación.

Necesitamos dos elementos:

  • Una clase LogToFile que contenga un método que modifique el fichero de LOG.
  • Una clase UserController, que al lanzar el método login, llame al método del Logger para realizar la acción.

Posteriormente, desde la aplicación, codificaremos la acción de login.

En este caso no vamos a prestar mucha atención al control de variables, lógica, etc.

Vamos a ver el código de nuestra clase LogToFile:

class LogToFile{

    public function execute($message){
    
        //volcamos información a archivo
    
    }

}

Nuestra clase encargada de controlar al usuario, recibiría en el constructor la clase encargada del LOG (del tipo LogToFile). Dentro del constructor asignaremos esta clase a una propiedad que sólo podrá ser accedido desde el mismo objeto y cuando se lanza su método de login, haría un volcado al archivo de texto.

class UserController{

    private $logger;
    
    public function __construct(LogToFile $logger){
    
        $this->logger = $logger;
    
    }
    
    public function login($user, $pass){
    
        //lógica de login
        
        $this->logger->execute($user . ' ha hecho login');
    
    }

}

Por último, la aplicación realizará una instancia de la clase UserController y realizará la acción de login.

$user = new USerController (new LogToFile);

$user->login($user, $pass);

Hasta aquí todo correcto.

Hemos utilizado Programación Orientada a Objetos, cada clase tiene su responsabilidad, etc.

¡Bien de código limpio!

Pero, ¿qué pasa si ahora se nos exige que en lugar de de realizar la acción de LOG contra un fichero de texto, se haga contra una base de datos?

Pues que nos encontramos con que no sólo tendremos que cambiar el código de nuestra aplicación, sino que tendremos que buscar en cada una de las clases las referencias a este LogToFile para cambiarlos, haciendo referencia a una clase como esta:

class LogToDatabase{

    public function execute($message){
    
        //volcamos información a base de datos
    
    }

}

Vamos a ver cómo quedaría nuestra aplicación después del cambio que implica la escritura en Base de Datos del login de usuario:

class LogToFile{

    public function execute($message){
    
        //volcamos información a archivo
    
    }

}

class LogToDatabase{

    public function execute($message){
    
        //volcamos información a base de datos
    
    }

}

class UserController{

    private $logger;
    
    public function __construct(LogToDatabase $logger){
    
        $this->logger = $logger;
    
    }
    
    public function login($user, $pass){
    
        //lógica de login
        
        $this->logger->execute($user . ' ha hecho login');
    
    }

}

$user = new USerController (new LogToDatabase);

$user->login($user, $pass);

En nuestra aplicación puede parecer una tarea ‘no muy tediosa’. Simplemente tenemos que cambiar la referencia a LogToFile dentro de la clase UserController (en el constructor).

Pero, ¿qué pasa si nuestra aplicación es mucho más extensa?

Pues que corremos el riesgo de pasar por alto referencias a la clase antigua (LogToFile), que pueden hacer que nuestra aplicación lance algún error o en definitiva, que no funcione como esperamos.

Además, ¿quién dice que no queremos mantener la clase LogToFile para usarla en un futuro o queremos que se siga manteniendo la funcionalidad de escribir a archivo para algunas partes de la aplicación?

Pues bien, aquí es donde vemos la necesidad de aplicar el principio Open/Close.

Recordad: Las clases deben estar abiertas a la extensión pero no a la modificación.

La aplicación del principio de buenas prácticas

Para poder ceñirnos al principio vamos a implementar un concepto ampliamente utilizado en la Programación Orientada a objetos:

La interfaz (interface en inglés).

La interfaz no es más que la declaración de un contrato al que todas las clases que implementen el mismo tienen que ceñirse.

Esto, trasladado al mundo de la POO, no significa nada más que una declaración que está un nivel por encima de las clases que lo implementan para obligarlas a declarar una serie de métodos obligatoriamente.

Dicho a bote pronto es algo así como:

Si quieres pertenecer al grupo molón que implementa esta interfaz, a la fuerza tienes que tener declarados estos métodos.

Vamos a ver cómo se implementaría una interfaz en nuestra aplicación. Empezamos con la declaración de la misma:

interface Logger{

    public function execute($message);

}

Como podéis ver, la lógica del método a implementar obligatoriamente, no se encuentra dentro de la interfaz. Sólo se realiza una declaración de los métodos a incluir dentro de las clases que implementen la misma.

La forma de especificar que una clase está ceñida al contrato que acabamos de declarar es la siguiente:

class LogToFile implements Logger{

    public function execute($message){
    
        //volcamos información a archivo
    
    }

}

Seguro que os estaréis preguntando:

¿en qué cambia esto nuestra aplicación y qué nos ahorra la implementación de una interfaz, Juanjo?

El cambio principal que hace que veamos la utilidad de una interfaz se encuentra dentro de la clase controladora del usuario: UserController.

Fijaos como cambia la definición del constructor en nuestra clase:

class UserController{

    private $logger;
    
    public function __construct(Logger $logger){
    
        $this->logger = $logger;
    
    }
    
    public function login($user, $pass){
    
        //lógica de login
        
        $this->logger->execute($user . ' ha hecho login');
    
    }

}

Lo único que ha cambiado es el tipado de la variable que recibe el constructor, ahora es del tipo de la interfaz y no de un tipo de logger específico como pudiera ser LogToFile.

Nuestra aplicación completa quedaría así:

interface Logger{

    public function execute($message);

}

class LogToFile implements Logger{

    public function execute($message){
    
        //volcamos información a archivo
    
    }

}

class LogToDatabase implements Logger{

    public function execute($message){
    
        //volcamos información a archivo
    
    }

}

class UserController{

    private $logger;
    
    public function __construct(Logger $logger){
    
        $this->logger = $logger;
    
    }
    
    public function login($user, $pass){
    
        //lógica de login
        
        $this->logger->execute($user . ' ha hecho login');
    
    }

}

$user = new USerController (new LogToFile);

$user->login($user, $pass);

Si os fijáis bien, seguimos inyectando una clase LogToFile en el código de nuestra aplicación, pero ahora no tendremos que cambiar la lógica de UserController cada vez que cambiemos de tipo de LOG.

Basta con declarar una clase que implemente la interfaz e inyectarla a la instanciación de la clase controladora del usuario, ya que lo que acepta el constructor es una clase del tipo del interfaz (todas las clases que implemente el mismo).

El principio se ve totalmente implementado ya que en este caso no tenemos que cambiar la clase sino ampliarla a través de una funcionalidad que implementan todos los lenguajes que se ajusten a la lógica de Programación Orientada a Objetos, los interfaces.

Recordad que tenéis todo el código referenciado en los manuales de buenas prácticas en GitHub.


Si os ha gustado el concepto y queréis conocer más sobre la Programación Orientada a Objetos, os invito a que veáis una sesión en directo dentro de la zona premium de Daniel Primo en la que se repasan las Bases de Programación Orientada a objetos en PHP.

Si queréis ampliar más conocimientos sobre los principios S.O.L.I.D. enfocado a principiantes, el episodio de Web Reactiva número 93, da un excelente enfoque sobre la implementación de estas buenas prácticas.

También deciros que el ejemplo empleado en este tutorial ha sido obtenido de la web de Jeffrey Way, LaraCasts.

¡Nos vemos en el siguiente fragmento de conocimiento, equipo!

Escrito originalmente por: Juan José Ramos

Imagen de Daniel Primo

Daniel Primo

CEO en pantuflas de Web Reactiva. Programador y formador en tecnologías que cambian el mundo y a las personas. Activo en linkedin, en substack y canal @webreactiva en telegram

12 recursos para developers cada domingo en tu bandeja de entrada

Además de una skill práctica bien explicada, trucos para mejorar tu futuro profesional y una pizquita de humor útil para el resto de la semana. Gratis.