Los 5 patrones de diseño más comunes en aplicaciones PHP

Foto de Neil Thomas en Unsplash

Si crees que el patrón número uno es Singleton, ¡estás despedido! El patrón Singleton ya está en desuso, y no es deseado ni odiado.

Echemos un vistazo a los 5 patrones diseñados más utilizados en el mundo PHP en estos días.

Fábrica

Debes usar fábricas cuando quieras construir un objeto. Así es, construir y no crear. No desea tener una fábrica solo para crear un nuevo objeto. Cuando construye el objeto, primero lo crea y luego lo inicializa. Por lo general, requiere realizar múltiples pasos y aplicar cierta lógica. Con eso, tiene sentido tener todo eso en un solo lugar y reutilizarlo cada vez que necesite tener un nuevo objeto construido de la misma manera. Básicamente, ese es el punto del patrón de fábrica.

Es una buena idea tener una interfaz para su fábrica y que su código dependa de ella y no de una fábrica concreta. Con eso, puede reemplazar fácilmente una fábrica por otra cuando lo necesite.

interfaz FriendFactoryInterface {
    función pública create (): Friend
}

A continuación, implementamos nuestra interfaz de fábrica con la siguiente clase:

La clase FriendFactory implementa FriendFactoryInterface {
    función pública create (): Friend {
        
        $ amigo = nuevo amigo ();
        // inicializa a tu amigo
        return $ amigo;
    }
}

¡Es un patrón de diseño bastante simple pero potente!

Estrategia

Se utiliza para ocultar detalles de implementación de algoritmos necesarios para realizar una operación. Teniendo estrategias, el cliente puede elegir el algoritmo necesario sin conocer la implementación real y aplicarlo para realizar la operación.

Supongamos que necesitamos crear una biblioteca que transfiera los datos de una fuente de datos a otra. Por ejemplo, necesitamos transferir los datos de la base de datos al archivo csv, o de la hoja de cálculo al archivo json. ¿Cómo lo harías tú?

Primero, necesitamos crear estrategias respectivas para leer los datos de los almacenamientos. Vamos a llamarlos lectores. Luego, necesitamos crear estrategias respectivas para escribir los datos en los almacenamientos. Vamos a llamarlos escritores.

Por lo tanto, tendremos 2 lectores para leer los datos de la base de datos o de la hoja de cálculo. En consecuencia, tendremos 2 escritores para escribir los datos en el archivo csv o en el archivo json.

Importante: el cliente que trabajará con nuestras estrategias no debería preocuparse por sus implementaciones. Por lo tanto, también debemos definir interfaces para nuestras estrategias. De esa manera, el cliente solo sabrá acerca de los métodos definidos por las interfaces de estrategia y trabajará solo con ellos, y lo que sucede detrás de escena no es su problema.

Finalmente, necesitamos crear el cliente que seleccionará las estrategias necesarias en función de dónde y dónde necesita transferir los datos.

Veamos todo eso en acción:

interfaz ReaderInterface {
    función pública start (): void;
    función pública read (): array;
    función pública stop (): void;
}
interfaz WriterInterface {
   función pública start (): void;
   función pública write (array $ data): void;
   función pública stop (): void;
}
class DatabaseReader implementa ReaderInterface {
    ...
}
class SpreadsheetReader implementa ReaderInterface {
    ...
}
la clase CsvWriter implementa WriterInterface {
    ...
}
La clase JsonWriter implementa WriterInterface {
    ...
}
transformador de clase {
    
    ...
    transformación de función pública (string $ from, string $ to): void {
        $ reader = $ this-> findReader ($ from);
        $ escritor = $ this-> findWriter ($ a);
        
        $ lector-> inicio ();
        $ escritor-> inicio ();
        tratar {
            foreach ($ reader-> read () como $ row) {
                $ escritor-> escribir ($ fila);
            }
         } finalmente {
             $ escritor-> stop ();
             $ lector-> stop ();
         }
     }
     ...
}

Como puede ver, el transformador que es el cliente de nuestras estrategias realmente no se preocupa por las implementaciones con las que funciona. Lo único que le importa son los métodos definidos por nuestras interfaces estratégicas.

Adaptador

Se utiliza para convertir una interfaz externa en una interfaz común. Supongamos que en el proyecto obtiene los datos de algún almacenamiento utilizando la siguiente clase.

Almacenamiento de clase {
    $ fuente privada;
    
    función pública __constructor (AdapterInterface $ source) {
        $ this-> source = $ source;
    }
    función pública getOne (int $ id):? object {
        devuelve $ this-> source-> find ($ id);
    }
    
    función pública getAll (array $ criterios = []): Colección {
        devuelve $ this-> source-> findAll ($ criterios);
    }
}

Tenga en cuenta que el almacenamiento no funciona directamente con la fuente, sino que funciona con el adaptador de la fuente.

Además, el almacenamiento no sabe nada sobre adaptadores de concreto. Se refiere solo a la interfaz del adaptador. Por lo tanto, la implementación concreta del adaptador provisto es una caja negra completa para él.

Aquí hay un ejemplo de la interfaz del adaptador

Interface AdapterInterface {
    función pública find (int $ id):? object;
    función pública findAll (array $ criterios = []): Colección;
}

Ahora, supongamos que usamos alguna biblioteca para acceder a la base de datos MySQL. La biblioteca dicta su propia interfaz y tiene el siguiente aspecto:

$ fila = $ mysql-> fetchRow (...);
$ datos = $ mysql-> fetchAll (...);

Como puede ver, no podemos integrar esta biblioteca así en nuestro almacenamiento. Necesitamos crear un adaptador para ello como a continuación:

La clase MySqlAdapter implementa AdapterInterface {
    
     ...
     función pública find (int $ id):? object {
         
         $ data = $ this-> mysql-> fetchRow (['id' => $ id]);
         // alguna transformación de datos
     }
     función pública findAll (array $ criterios = []): Colección {
              
         $ data = $ this-> mysql-> fetchAll ($ criterios);
         // alguna transformación de datos
     }
   
     ...
}

Después de eso, podemos inyectarlo en el almacenamiento de la siguiente manera:

$ almacenamiento = nuevo almacenamiento (nuevo MySqlAdapter ($ mysql));

Si más tarde decidimos usar otra biblioteca en lugar de esa, solo tendremos que crear otro adaptador para esa biblioteca como lo hicimos anteriormente, y luego, inyectar el nuevo adaptador en el Almacenamiento. Como puede ver, para usar una biblioteca diferente para obtener los datos de la base de datos, no necesitamos tocar nada dentro de la clase Almacenamiento. ¡Ese es el poder del patrón de diseño del adaptador!

Observador

Se utiliza para notificar al resto del sistema sobre ciertos eventos en cierto lugar. Para comprender mejor los beneficios de este patrón, revisemos dos soluciones del mismo problema.

Digamos que necesitamos crear teatro para mostrar películas a los críticos. Definimos la clase Teatro con el método presente. Antes de presentar la película, queremos enviar mensajes a los teléfonos celulares de los críticos. Luego, en medio de la película, queremos detener la película durante 5 minutos para que los críticos tengan un descanso. Finalmente, después de que termina la película, queremos pedirles a los críticos que dejen sus comentarios.

Veamos cómo se vería esto en el código:

Teatro de clase {
   
    función pública presente (Película $ película): nulo {
       
        $ críticos = $ película-> getCritics ();
        $ this-> messenger-> send ($ critics, '...');

        $ película-> play ();

        $ película-> pausa (5);
        $ this-> progress-> break ($ ​​críticos)
        $ película-> terminar ();

        $ this-> feedback-> request ($ critics);
    }
}

Se ve limpio y prometedor.

Ahora, después de un tiempo, el jefe nos dijo que antes de comenzar la película también queremos apagar las luces. Además, en medio de la película, cuando se detiene, queremos mostrar el anuncio. Finalmente, cuando termina la película, queremos comenzar la limpieza automática de la habitación.

Bueno, uno de los problemas aquí es que para lograr eso necesitamos modificar nuestra clase de Teatro, y eso rompe los principios SÓLIDOS. Particularmente, rompe el principio abierto / cerrado. Además, este enfoque hará que la clase de teatro dependa de varios servicios adicionales, lo que tampoco es bueno.

¿Qué pasa si ponemos las cosas al revés? En lugar de agregar más y más complejidad y dependencias a la clase de Teatro, distribuiremos la complejidad en todo el sistema y, con eso, reduciremos las dependencias de la clase de Teatro como un extra.

Así es como se verá esto en acción:

Teatro de clase {
    
    función pública presente (Película $ película): nulo {
        
        $ this-> getEventManager ()
            -> notificar (nuevo Evento (Evento :: INICIO, $ película));
        $ película-> play ();

        $ película-> pausa (5);
        $ this-> getEventManager ()
            -> notificar (nuevo Evento (Evento :: PAUSA, $ película));
        $ película-> terminar ();

        $ this-> getEventManager ()
            -> notificar (nuevo Evento (Evento :: FIN, $ película));
    }
}
$ teatro = nuevo teatro ();
$ teatro
    -> getEventManager ()
    -> listen (Evento :: START, nuevo MessagesListener ())
    -> escuchar (Evento :: START, nuevo LightsListener ())
    -> escuchar (Evento :: PAUSA, nuevo BreakListener ())
    -> escuchar (Evento :: PAUSA, nuevo AdvertisementListener ())
    -> escuchar (Evento :: FIN, nuevo FeedbackListener ())
    -> listen (Evento :: FIN, nuevo CleaningListener ());
$ teatro-> presente ($ película);

Como puede ver, el método actual se vuelve extremadamente sencillo. No le importa lo que sucede fuera de la clase. Simplemente hace lo que se supone que debe hacer y notifica al resto del sistema sobre los hechos. Cualquier cosa que esté interesada en esos hechos puede escuchar los eventos respectivos y ser notificado sobre ellos y hacer lo que tiene que hacer.

Con este enfoque, también se vuelve bastante fácil agregar complejidad adicional. Todo lo que tiene que hacer es crear un nuevo oyente y poner allí la lógica necesaria.

Espero que hayas encontrado útil el patrón Observador.

Decorador

Se utiliza cuando desea ajustar el comportamiento de un objeto en tiempo de ejecución y, con eso, reducir las herencias redundantes y el número de clases. Podrías preguntar por qué necesito eso. Bueno, podría explicarse mejor con ejemplos.

Digamos que tenemos clases de Ventana y Puerta, y ambas implementan OpenerInterface.

interfaz OpenerInterface {
    función pública open (): void;
}
clase Implementos de puerta OpenerInterface {
    función pública open (): void {
        // abre la puerta
    }
}
class Window implementa OpenerInterface {
    función pública open (): void {
        // abre la ventana
    }
}

Tanto las ventanas como las puertas tienen el mismo comportamiento para abrirse. Ahora, necesitamos otras puertas y ventanas con una funcionalidad adicional que les diga a los usuarios la temperatura exterior cuando abran las puertas o ventanas. Podemos resolver este problema con la herencia así:

clase SmartDoor extiende la puerta {
    función pública open (): void {
        padre :: abierto ();
        $ this-> temperatura ();
    }
}
clase SmartWindow extiende la ventana {
    función pública open (): void {
        padre :: abierto ();
        $ this-> temperatura ();
    }
}

Con todo, tenemos 4 clases en total por ahora. Sin embargo, con el patrón Decorator podríamos resolver este problema solo con 3 clases. Así es cómo:

La clase SmartOpener implementa OpenerInterface {
    
    abridor privado de $;
    función pública __construct (OpenerInterface $ opener) {
        $ this-> opener = $ opener;
    }
    
    función pública open (): void {
        $ this-> opener-> open ();
        $ this-> temperatura ();
    }
}
$ puerta = nueva puerta ();
$ ventana = nueva ventana ();
$ smartDoor = nuevo SmartOpener ($ puerta);
$ smartWindow = new SmartOpener ($ ventana);

Hemos introducido un nuevo tipo de abridor que actúa como un proxy pero con una funcionalidad adicional por encima. Eso es lo que hace el truco.

Espero que hayas encontrado este artículo útil e interesante. Si es así, no dude en aplaudir y compartirlo en las redes sociales.

¡Feliz codificación! :)