Patrones de diseño en PHP

Patrones de diseño en PHP

Estas Navidades he estado refrescando los patrones de diseño más habituales en PHP (sí, sigo picando en Symfony pese a que la moda sea NodeJS o Python) por aquello de evitar reinventar la rueda siempre que pueda y he decidido recoger los principales en este artículo, explicando para cada uno de ellos en qué consisten y en qué casos podéis emplearlos. Espero que os sirva o que al menos aprendáis alguno nuevo, ya que la mayoría son, por supuesto, extrapolables a otros lenguajes. ¡Vamos a ello!

1. Active Record

El patrón active record consiste en mapear las filas de una tabla o vista en base de datos a un objeto. Por tanto, el objeto se convierte en el responsable de obtener, crear, actualizar y borrar el correspondiente registro asociado en base de datos. En este patrón, cada columna de base de datos se corresponde con una propiedad del objeto sin ningún tipo de manipulación intermedia.

Probablemente este patrón os suene cuando trabajáis con ORMs (Object Relational Mapper) como Doctrine.

Diagrama

Active Record Pattern

Pros

  • Evita la duplicación de código
  • Centraliza el acceso a base de datos.

Contras

  • Los objetos se encuentran fuertemente acoplados a la base de datos, por lo que un cambio en el esquema tiene que traducirse obligatoriamente en un cambio en el objeto asociado y viceversa.
  • Es complicado realizar pruebas unitarias sin recurrir a una base de datos o a realizar mocks de la misma

2. Adapter Pattern

Pese a que suele ser uno de los patrones más sencillos de entender y de aplicar, podemos considerarlo uno de los más útiles como veremos a continuación.

¿Qué resuelve?

El patrón adapter resuelve la situación en la que nos vemos con dos componentes que no “encajan” cuando tratamos de emplearlos juntos. Imaginad un enchufe americano y un conector europeo… necesitamos un adaptador intermedio que nos permita conectarnos a la red. Ese mismo concepto es el que se traslada para definir el patrón adapter.

Diagrama

Adapter pattern

Pros

Siguiendo con la analogía del enchufe, el patrón adapter ajusta la interfaz de uno de los dos componentes o clases para adaptarlo al otro, de modo que podamos usar los dos componentes sin tener que recurrir a la horrible solución de modificar el código interno de uno de ellos (nada como tener que mantener para siempre el código de un componente que tú no has escrito).

Contras

  • Sin embargo, el patrón adapter no deja de ser una mera capa que oculta un mal diseño inicial, por lo que si todos los componentes los hemos diseñado nosotros no sería lógico tener que recurrir a él.

3. Decorator Pattern

¿Qué resuelve?

El patrón decorador aborda el problema de añadir comportamiento específico a objetos de una clase. En POO lo más habitual cuando queremos añadir funcionalidad extra a una clase es recurrir a la herencia, es decir, extender una clase e implementar los métodos que necesitemos.

Sin embargo, en PHP no podemos heredar de más de una clase, es decir, una clase C no puede heredar a la vez de A y B. Es más, también podemos vernos en la situación de que sólo necesitemos añadir comportamiento a objetos determinados de la clase pero no a la clase en sí misma (es decir, a todos los objetos).

Es aquí donde aparece el decorator pattern, el cual nos va a permitir añadir comportamiento específico a los objetos de una clase sin tener que añadírselo a la clase a la que pertenece.

Diagrama

Decorator Pattern

Como veis en el diagrama, para implementar un decorador lo que hacemos es lo siguiente.

  1. Extender la clase original Component en una llamada Decorator.
  2. En la clase Decorator añadir un campo que sea un puntero a un objeto de la clase Component. Por tanto, el constructor de la clase Decorator deberá recibir una instancia de la clase Component para inicializar dicho campo.
  3. En la clase Decorator, redirigir todos los métodos de la clase Component a la instancia que fue pasada en el constructor.
  4. En la clase ConcreteDecorator, que hereda de la clase Decorator, reimplementar los métodos que necesitemos a conveniencia para añadirles funcionalidad.

Pros

  • Añadir o alterar el comportamiento de una interfaz en tiempo de ejecución

Contras

  • Nuevamente recurrir a este patrón incrementa la dificultad de realizar pruebas unitarias debido a que no todos los objetos de la clase comparten el comportamiento de la clase a la que pertenencen.

4. Factory Pattern

¿Qué resuelve?

El factory pattern aborda el problema de crear objetos sin tener que especificar la clase exacta a la que han de pertenecer y sin tener que acceder directamente a la lógica de creación.

Imaginad por ejemplo la conexión con una base de datos. Habitualmente, lo que nosotros necesitamos es un objeto que nos permita interactuar con la base de datos subyacente independientemente de que sea MySQL, SQL Server o SQLite. Aunque acabemos usando el objeto de conexión de una forma similar (todos dispondrán de los métodos query, insert, delete… ), la lógica de creación puede diferir bastante y es aquí donde entra este patrón de diseño, permitiéndonos obtener un objeto conexión dentro de una clase sin tener que bajar a los detalles de la instanciación de dicho objeto.

Diagrama

Abstract Factory Pattern

Como podéis ver, la clase Client necesita un objeto perteneciente a la interfaz ProductoA, la cual es extendida por las clases ProductoA1 y ProductoA2. Lo que nos dice el patrón de diseño abstract factory es que, en el constructor de la clase Cliente, recibamos una instancia de la interfaz AbstractFactory, la cual es extendida por las clases FactoriaConcreta1 y FactoriaConcreta2, cada una con sendos métodos para crear instancias de la interfaz ProductoA (ProductoA1 y ProductoA2 respectivamente). Lo mismo sucede para productos pertenecientes a la interfaz ProductoB.

Existe también otra alternativa a este patrón de diseño conocido como factory method, del cual podéis leer más aquí:

Código de ejemplo

class Client {
private AbstractFactory factory;

public A(AbstractFactory factory) {
this.factory = factory;
}

public void doSomething() {
ProductA p = factory.crearProductoA();
p.foo();
}
}

interface AbstractFactory {
ProductoA crearProductoA();
}

Pros

  • Permite estandarizar las interfaces al delegar la lógica de creación del objeto a una clase.
  • Permite desacoplar la clase de la lógica de creación del objeto
  • Responde al principio Open/Close, al permitirnos modificar en las subclases el tipo de objeto creado sin tener que modificar el código interno.

Desventajas

  • Cuando comenzamos a emplear este patrón, es necesario refactorizar el código para reemplazar la anterior lógica de construcción de objetos.
  • Puesto que la factoría la emplearemos para, habitualmente, crear objetos que heredan de una clase común, puede que necesitemos bastante boilerplate code en cada una de nuestras subclases.

5. Mock Objects

¿Qué resuelve?

Cuando estamos desarrollando cierta funcionalidad, es probable que nos encontremos con lo que se conocen como side effects (por ejemplo enviar un email al usuario que acaba de registrarse). Pese a que podamos recurrir a trabajar con sandboxes para realizar pruebas, conforme la aplicación estos entornos de prueba son más difíciles de mantener, y es aquí donde aparece este patrón de diseño.

Los Mock Objects son simulaciones de objetos que replican el comportamiento de objetos reales pero de forma controlada, de modo que podamos estar 100% seguros de cómo van a comportarse al realizar ciertas acciones.

Su aplicación más inmediata es al realizar tests unitarios. Volviendo al ejemplo de los emails de registro, podemos recurrir a los Mock Objects para obtener el HTML del email enviado sin que el sistema lo envíe al destinatario.

Pros

  • Permiten realizar pruebas unitarias que involucran objetos cuyas características hacen imposible de integrar en un test, por ejemplo, posee estados que son muy difíciles de reproducir.
  • Permiten realizar pruebas unitarias sobre clases que todavía no han sido implementadas pero para las que sí conocemos su comportamiento.

Contras

  • Se encuentran demasiado acoplados a la clase que replican, llegando al punto de que a veces es fácil encontrarse con Mock Objects que no solo exponen un comportamiento externo similar, sino que también replican comportamiento interno, provocando que sean bastante difíciles de mantener.
  • Dado que no dejan de ser copias que imitan al original, es posible que determinados casos límites de los tests unitarios den falsos positivos al prescindir por completo de los side effects.

6. MVC: Model — View — Controller

¿Qué resuelve?

Este patrón de diseño (que tuvo su auge durante la década de los 90 y 2000) tiene como objetivo abordar el problema de la separación de conceptos, es decir, definir de forma clara qué ha de hacer cada uno de los componentes que aparecen en nuestra aplicación. Esto por supuesto se acabará traduciendo en una mejor legibilidad y reutilización del código que escribamos.

El patrón MVC surgió para evitar la proliferación de programas en los que un mismo archivo contiene la lógica para la entrada / salida de datos, las operaciones realizadas sobre los datos con los que el programa trabaja y el sistema para el almacenamiento de los mismos, algo que dificulta enormemente su mantenimiento y escalabilidad; realmente parece más un cocktail que una aplicación.

Si queréis un ejemplo basta con que penséis en esos archivos que todavía rondan en por ahí en estado salvaje donde el código HTML aparece entremezclado con lógica escrita en PHP y accesos a base de datos distribuidos a lo largo de las n mil líneas del archivo.

Diagrama

Como podéis ver en el diagrama, este patrón se compone de 3 elementos principales:

  • Modelo, el cual representa los datos con los que la aplicación opera así como toda la lógica para su almacenamiento y su recuperación. Se encarga de enviar a la vista la información para que sea mostrada.
  • Vista, la cual se encarga de representar los datos del modelo procedentes del controlador y de recoger todas las interacciones del usuario sobre los elementos allí mostrados para enviarlos al controlador.
  • Controlador, que se encarga de procesar las interacciones del usuario con la vista y enviar las acciones al modelo para manipular los datos.
Secuencia en un MVC

Pros

  • Permite paralelizar el desarrollo. Gracias a que la lógica queda separada, es posible que varios desarrolladores trabajen en paralelo en los diferentes componentes del MVC.
  • Gracias a la separación de conceptos es posible crear múltiples vistas del modelo sin tener que recurrir a la duplicación de código.
  • Permite realizar pruebas unitarias mucho más efectivas gracias a que cada componente puede ser probado por separado.

Contras

  • Incrementa la complejidad, al ser necesaria la creación de la lógica para la comunicación entre los 3 componentes del patrón.
  • Debido a la separación de conceptos, cada componente es probable que tienda a evolucionar por separado, lo cual supone la introducción de nuevas tecnologías y la necesidad de especialización en cada una de ellas.
  • La vista termina dependiendo tanto del controlador como del modelo, por lo que es frecuente ver un fuerte acoplamiento en ella con respecto a ambos componentes.

7. ADR. Action — Domain — Responder

¿Qué resuelve?

Con la popularización de las aplicaciones web, el modelo MVC terminó por refinarse en un nuevo patrón de diseño propuesto por Paul M. Jones: el patrón ADR, el cual se ajusta mejor a los problemas que nos puede tocar abordar en desarrollos de aplicaciones de este tipo.

Su implementación tiene como objetivo modelar el flujo request-response en una comunicación HTTP del mismo modo que el patrón MVC realizaba para aplicaciones de escritorio, pero ajustándose mejor al caso de uso de aplicaciones web.

Diagrama

Action-Domain-Response

Como veis en el diagrama, en este patrón de nuevo vuelven a intervenir 3 elementos:

  • Action, el cual recoge las peticiones HTTP y emplea dicho input para interactuar con el modelo. Finalizadas todas las operaciones, recoge el output del dominio para pasárselo a un único responder. Podríamos considerarlo como el equivalente al controlador del patrón MVC.
  • Domain, encargado de modificar el estado de la aplicación, interactuando con el sistema de almacenamiento y manipulando los datos. Es el equivalente al modelo del patrón MVC.
  • Responder, encargado de construir la respuesta HTTP en base al output del domain que ha sido enviado por el elemento action.

Diferencias entre el patrón ADR y el patrón MVC

Pese a que ambos patrones de diseño son bastante similares, existen entre ellos diferencias sustanciales que los hacen más adecuados para el caso de uso para el que fueron pensados:

  • Pese a que el domain y el model son bastante similares, en el patrón MVC la vista puede enviar información de cara a modificar el modelo, mientras que en el patrón ADR es solo el elemento action el encargado de pasar información al dominio.
  • En el patrón MVC el controlador se encarga de todo el proceso de generación de la respuesta (cookies, cabeceras…), mientras que en el patrón ADR el elemento responder es 100% responsable de elaborar la respuesta enviada al cliente.
  • Generalmente, en el patrón MVC el controlador posee distintos métodos en la misma clase, cada uno de ellos encargados de realizar una tarea. Sin embargo, en el patrón ADR, un action es una clase autoinvocable con un único método expuesto (y que implementa el método __invoke ) por lo que por cada acción que queramos crear deberemos añadir una nueva clase.

Pros

  • Responde al principio SOLID de Single Responsability Principle
  • Permite una mejor organización del proyecto y una mejor convención de nombres
  • Mejora la testeabilidad del proyecto, gracias a que cada clase contiene un único método.

Bola extra

Si queréis ver una implementación de este patrón bajo Symfony podéis consultar este repositorio:

en el cual aparece implementada una simple aplicación Blog mediante este patrón.

8. Publish — Subscribe

¿Qué resuelve?

El patrón publish — subscribe surge con el objetivo de modelar un sistema de comunicación en el que existen por un lado senders, encargados de publicar mensajes y, por el otro, subscribers, los cuales reciben los mensajes acerca del “tema” por el que han mostrado interés.

En este patrón, los publishers no conocen los subscribers que se han suscrito a sus mensajes, es decir, no les envían directamente los mensajes sino que categorizan sus mensajes en clases que son transmitidas a través de un canal de comunicación.

Diagrama

Publish — Subscriber diagram

Como se puede apreciar en el diagrama, no todos los subscribers reciben todos los mensajes, sino sólo aquellos pertenecientes a la categoría (representada por una clase) a la que se hayan suscrito. Este proceso es conocido como filtering y generalmente es realizado por un broker o canal de comunicación.

Es en este broker donde los subscribers se registran para recibir los mensajes de una determinada categoría.

Pros

  • Elimina el acoplamiento que puede surgir entre publishsers y subscribers, ya que cada una de estas partes ignora la existencia de la otra. De hecho, no es necesario ni siquiera que ambos se estén ejecutando a la vez.
  • Mejora la escalabilidad de los proyectos ya que permite la paralelización de operaciones y el cacheo de los mensajes enviados.

Contras

  • Dado que no existe confirmación de recepción por parte de los susbscribers, este patrón no puede garantizar que los mensajes se envíen correctamente.
  • Es frecuente asumir que para cada publisher existe al mínimo un subscriber y viceversa, lo cual puede provocar que algunas partes de la aplicación no funcionen correctamente en ausencia de uno de ellos.

9. Dependency injection

¿Qué resuelve?

Este patrón implementa un sistema por el cual un objeto se encarga de proveer de las dependencias al resto de objetos (clientes) que así lo requieran.

Los clientes reciben sus dependencias por medio del constructor (constructor injection) o mediante un método setter (setter injection), delegando la tarea de buscar y encontrar dichos objetos en un Container. De este modo queda separada la lógica de creación de las depedencias del cliente de su comportamiento, lo cual se traduce en aplicaciones con un acomplamiento más reducido y con clases que responden a los principios Open/Close y Dependency Inversion.

Diagrama

Inyección de dependencias

Los elementos que participan en este patrón son:

  • Las interfaces, que definen la interfaz común a servicios del mismo “tipo”. Por ejemplo, servicios que modelan el acceso a base de datos.
  • Los servicios, es decir, las dependencias que implementan una interfaz concreta y que serán pasados por el injector a los objetos cliente.
  • El injector, el objeto encargado de suministrar a cada cliente la dependencia (implementación de una determinada interfaz) que necesita.
  • El cliente, objeto que necesita una determinada dependencia pero que delega la construcción de la misma en el injector.

Pros

  • Gracias a la implementación de este patrón, las clases que así creemos responder al principio SOLID open/close, ya que el cliente tan solo es “consciente” de la interfaz de la implementación, no de su implementación concreta. Por tanto, pasar por ejemplo de un sistema de almacenamiento a otro se realizaría de forma inmediata cambiando tan solo la clase pasada por el injector.
  • Además, gracias a que las dependencias de un mismo tipo (servicios) implementan una misma interfaz, los clientes cumplen el principio de inversión de dependencias, al depender tan solo de las abstracciones de las mismas.
  • Las clases que reciben inyección de dependencias están desacopladas de la implementación de las mismas, facilitando la modificación de una dependencia por otra.

Cons

  • Al realizar test unitarios es probable que tengamos que recurrir a mocks de las dependencias si queremos evitar los side effects de las dependencias.
  • Puesto que los clientes desconocen los detalles de la implementación que están empleando, también se desconoce los side effects que pueden producirse.
  • Los constructores de los objetos pasan a ser bastante más largos, al recibir por medio de ellos cada una de las dependencias necesarias.

10. Command

¿Qué resuelve?

El patrón Command tiene como objetivo definir la forma en que los objetos interactúan entre ellos. Concretamente, este patrón permite solicitar una determinada operación a un objeto, parametrizando dicha petición en un objeto.

Diagrama

Command Pattern

Supongamos por ejemplo que queremos realizar una acción tal como crear un artículo de un blog. Si queremos resolverlo mediante este patrón, necesitaremos los siguientes actores:

  • Una interfaz Command que defina el método execute. En PHP, por suerte, tenemos directamente el método mágico __invoke el cual puede actuar como alternativa, por lo que no sería necesario definir dicho método.
  • Una clase CreateArticleCommand, que implemente la interfaz anterior y que se encargará de realizar las acciones concretas para crear un artículo. Puede ser perfectamente un servicio para aprovecharse de la inyección de dependencias si estáis trabajando con Symfony
  • Un Client, el cual será el encargado de llamar al comando para ejecturarlo. En nuestro caso sería el controlador asociado a la ruta POST encargada de crear un artículo.

De esta forma conseguiremos tener encapsulada la creación de un artículo en un comando, quedando aislada esa funcionalidad de la lógica del controlador.

Pros

  • Bajo acoplamiento del código especialmente si estamos trabajando con Symfony ya que los comandos recibirán las clases a utilizar mediante inversión de dependencias, consiguiendo que realizar cambios sea una tarea prácticamente inmediata.
  • Reutilización, gracias a que nuestros comandos podremos emplearlos en cualquier parte del código
  • Testeabilidad. Otra de las ventajas de implementar este patrón es que los comandos así creados son muy fáciles de testear pues realizan una única opción y generalmente recibirán sus dependencias mediante inyección de dependencias.
  • Escalabilidad. En el caso de que los comandos realicen operaciones muy pesadas es posible crear un dispatcher encargado de ejecutar los comandos en una cola, e, incluso, definir comandos que se ejecuten en los lenguajes más adecuados para las operaciones que realicen.

Contras

  • La pega más habitual que podemos encontrarnos al implementar este patrón es que el número de clases de nuestro proyecto se verá incrementada, especialmente si la información a los comandos las pasamos mediante clases DTO’s lo cual hará necesaria una adecuada organización de los archivos para no volvernos locos.

Conclusión

Por supuesto, estos son solo algunos de los patrones que más frecuentemente podemos encontrarnos en PHP o en algunos de sus frameworks más conocidos.

Recordad que los patrones de diseño establecen soluciones para problemas comunes, por lo que conocerlos y saber aplicarlos puede evitarnos reinventar la rueda en más de una ocasión.

Si queréis leer más sobre ellos, mi libro favorito es:

Se lee muy muy bien y contiene múltiples ejemplos y ejercicios para que practiquéis con ellos. Espero que os sirva de ayuda!