Tutorial de Patrones de Diseño en PHP

PHP

En este tutorial veremos cuáles son los patrones de diseño más utilizados en PHP, además de exponer varios ejemplos para que puedes ver cómo funcionan y dominarlos. Si te consideras un desarrollador con cierto nivel, es necesarios comprenderlos y utilizarlos siempre y cuando sean necesarios.

Los patrones de diseño son arquitecturas que usas a la hora de organizar tu código, haciendo que sea reusable, extensible, legible, más limpios y más elegante. Cada patrón de diseño trata de resolver un problema, así que no existe ningún patrón mágico que puedes usar en todos los casos.

A continuación explicaremos cuáles son los patrones más utilizados, cuál es el propósito de cada uno y cuándo deben utilizarse.

El patrón Singleton

Este patrón se usa cuando te quieres asegurar de que una clase tenga una y solo una instancia, de forma que existe un único punto de acceso a la misma. Siempre que se cree una instancia de la clase, se devolverá una nueva instancia si no se había creado ninguna, o esa misma instancia si ya estaba creada anteriormente. En ningún caso se devolverá una segunda instancia.

Este patrón Singleton se usa en los siguientes casos:

  • Cuando se quiere que exista una y solo una instancia de la clase.
  • Cuando necesitas controlar el acceso a recursos compartidos, como pueden ser una base de datos o un sistema de caché.

El patrón Singleton se logra mediante un método estático que se encarga de devolver una instancia de una clase, almacenada en una variable estática. El constructor se suele declarar como privado, de forma que solo se pueda acceder a él desde el interior de la propia clase, evitando la creación de instancias desde fuera.

A continuación vamos a ver un ejemplo:

class DB {

    // Almacenamiento estático de la instancia
    private static $instancia;

    private function __construct()
    {
        // El constructor se declara como privado
    }

    public static function getInstancia()
    {
        // Se crea  una nueva instancia siempre que no exista
        if (!self::$instancia) {
            self::$instancia= new self();
        }

        // En caso contrario se devuelve la instancia existente
        return self::$instancia;
    }
}

// En ambos casos la instancia asignada es la misma
$singletonDB1 = DB::getInstancia();
$singletonDB2 = DB::getInstancia();

// Puede comprobarse de este modo
var_dump($singletonDB1 === $singletonDB2);

El patrón Repositorio (Repository)

El patrón Repository se usa para separar la lógica que se usa para gestionar los datos de un modelo desde una base de datos o desde otro método de almacenamiento de la lógica de negocio de la aplicación. Un repositorio es un intermediario que permite que el código que interactúa con la base de datos sea más limpio, más mantenible y más fácil de testear.

Este patrón es especialmente útil en aplicaciones que requieren interactuar con una o más fuentes de datos. Se suele usar en el conocido como Domain Driven Development (DDD), en donde la separación de las funcionalidades de clases es una consideración clave para la escalabilidad, la mantenibilidad y la flexibilidad del proyecto.

Vamos a ver un ejemplo, comenzando por la definición de las entidades de dominio. Para empezar vamos a definir una clase Usuario:

public class Usuario
{
    public $id;
    public $nombre;
    public $email;
}

Ahora vamos a definir una interfaz llamada RepositorioUsuarioInterface que especifique los métodos que se usan para recuperar y gestionar los usuarios desde una fuente. Esta fuente, podría ser por ejemplo una base de datos:

interface RepositorioUsuarioInterface
{
    public function getById($id);
    public function getAll();
    public function add(Usuario $usuario);
    public function update(Usuario $usuario);
    public function delete($id);
}

Ahora vamos a implementar un repositorio concreto al que llamaremos RepositorioUsuario. El repositorio gestionará la interacción real con la base de datos. Para este ejemplo, asumiremos que se utiliza PDO para la conexión a la base de datos:

class RepositorioUsuario implements RepositorioUsuarioInterface
{
    protected $db;

    public function __construct(PDO $db)
    {
        $this->db = $db;
    }

    public function getById($id)
    {
        $stmt = $this->db->prepare("SELECT * FROM usuarios WHERE id = ?");
        $stmt->execute([$id]);
        $stmt->setFetchMode(PDO::FETCH_CLASS, 'Usuario');
        return $stmt->fetch();
    }

    public function getAll()
    {
        $stmt = $this->db->query("SELECT * FROM usuarios");
        return $stmt->fetchAll(PDO::FETCH_CLASS, 'Usuario');
    }

    public function add(Usuario $usuario)
    {
        $stmt = $this->db->prepare("INSERT INTO usuarios (nombre, email) VALUES (?, ?)");
        $stmt->execute([$usuario->nombre, $usuario->email]);
    }

    public function update(Usuario $usuario)
    {
        $stmt = $this->db->prepare("UPDATE usuarios SET nombre = ?, email = ? WHERE id = ?");
        $stmt->execute([$usuario->nombre, $usuario->email, $usuario->id]);
    }

    public function delete($id)
    {
        $stmt = $this->db->prepare("DELETE FROM usuarios WHERE id = ?");
        $stmt->execute([$id]);
    }
}

Ahora ya podemos usar el repositorio dentro de la lógica de negocio. Aquí puedes ver un ejemplo de un servicio que se encarga de gestionar usuarios que utiliza el RepositorioUsuario:

class GestorUsuarios
{
    protected $repositorioUsuario;

    public function __construct(RepositorioUsuarioInterface $repositorioUsuario)
    {
        $this->repositorioUsuario= $repositorioUsuario;
    }

    public function agregarUsuario($nombre, $email)
    {
        $usuario = new Usuario();
        $usuario->nombre = $nombre;
        $usuario->email = $email;
        $this->repositorioUsuario->add($usuario);
    }

    // Métodos adicionales de gestión de usuarios...
}

Esta patrón Repository logra que el código sea más modular, fácil de mantener y de testear. Este patrón usa generalmente los siguientes componentes:

  • Repositorio: Se trata de una interfaz que define los métodos para acceder a los datos. Estos métodos encapsulan las consultas a la base de datos y ocultan los detalles de la implementación de acceso a datos del resto de la aplicación.
  • Repositorio Concreto: Es una implementación de la interfaz del repositorio que contiene la lógica específica para acceder a los datos de método un almacenamiento en particular, como por ejemplo una base de datos MySQL, una base de datos MongoDB o un simple archivo XML.
  • Entidades: Clases que representan los objetos del modelo de negocio, como por ejemplo Usuario o Producto. La aplicación opera sobre estos objetos.
  • Capa de Acceso a Datos: Contiene el código que gestiona la conexión con la base de datos y la ejecución de consultas. En el patrón Repository, esta lógica suele encapsularse dentro de los repositorios concretos.

El propósito principal del patrón Repositorio es desacoplar la lógica de negocio de la aplicación de los detalles de cómo se accede y se almacenan los datos. Esto se logra mediante la creación de una capa de abstracción que separa el código que accede a la base de datos del resto de la aplicación.

Entre los beneficios del patrón Repository se encuentra la abstracción y el encapsulamiento de los datos. De este modo se ocultan los detalles de la implementación de acceso a los datos, permitiendo que el resto de la aplicación opere con abstracciones de alto nivel.

Otra de sus ventajas es la mantenibilidad, ya que se centraliza el acceso a los datos, lo que facilita la gestión y actualización del código relacionado con la base de datos. Además, esto permite que el código resulte más fácil de testear al permitir la sustitución de implementaciones del repositorio con facilidad.

Por último, la mayor ventaja del patrón Repository es que gracias a él podrás cambiar fácilmente el mecanismo de almacenamiento subyacente sin afectar la lógica de negocio de la aplicación. Por ejemplo, podrás tener diferentes implementaciones del repositorio; una para MongoDB y otra para MySQL, que operarán del mismo modo desde tus servicios o desde tus controladores.

El patrón Factoría (Factory)

Para usar este patrón, se debe crear una interfaz que defina la implementación de las clases que la usarán. Esta interfaz definirá el modo en el que se crearán objetos de dichas clases, alterando así el tipo de objetos creados. El mejor modo de entenderlo es imaginarse una factoría real que pueda crear varios tipos de productos.

Para entenderlo mejor, vamos a partir de una interfaz a la que llamaremos Producto, en cargada de definir la implementación de un producto:

interface ProductoInterface
{
    public function getTipo();
}

Ahora vamos a crear dos tipos de producto que implementen la interfaz ProductoInterface que hemos creado en el paso anterior:

class ProductoA implements ProductoInterface
{
    public function getTipo()
    {
        return 'Tipo A';
    }
}

class ProductoB implements ProductoInterface
{
    public function getTipo()
    {
        return 'Tipo B';
    }
}

Ahora vamos a definir la interfaz de una factoría que se encargue de crear los productos anteriores:

interface FactoriaProductoInterface
{
    public function crear(): ProductoInterface;
}

La interfaz que hemos creado dispone de un método que se usa para crear productos genéricos, sin especificar su tipo.

A continuación vamos a crear una implementación de la interfaz FactoriaProductoInterface. Esta implementación puede devolver únicamente un tipo de producto, ya sea ProductoA o ProductoB, de forma que en tiempo de ejecución se pueda asignar una u otra factoría. También es posible que la factoría devuelva dinámicamente una nueva instancia de un tipo u otro de producto en base a ciertas condiciones. En este ejemplo, vamos a crear la implementación FactoriaProducto de la factoría:

class FactoriaProducto implements FactoriaProductoInterface
{
    public function crearProducto(): ProductoInterface {
        return new ProductoA();
    }
}

Ahora vamos a crear una instancia de la factoría:

$factoriaProducto = new FactoriaProducto();

Ahora vamos a crear un producto del tipo definido por la factoría:

$producto = $factoria->crearProducto();

Si ahora mostramos el tipo de producto, veremos que el tipo devuelto es Tipo A:

echo $producto->getTipo(); // Tipo A

Tal y como ves, las este patrón implica los siguientes elementos:

  • Interfaz de Producto: Define la interfaz de los objetos que el método de fabricación crea.
  • Implementación de Producto: Implementa la interfaz de producto y define el objeto que será creado.
  • Interfaz de Factoría: Declara el método fabricación, que retorna un objeto de tipo producto. Puede ser abstracto o tener una implementación por defecto.
  • Implementación de la Factoría: Contiene el método de fabricación, que retorna una instancia de una implementación de un producto.

A continuación vamos a ver otro tipo de patrón de diseño de factoría existente.

El patrón Factoría Abstracta (Abstract Factory)

Este patrón es similar al patrón factoría que ya hemos visto. Este patrón se usa cuando necesitas crear familias de objetos relacionados sin especificar sus clases concretas, permitiendo que tu código sea independiente del método usado para su creación, de cómo se componen y de cómo se representan estos objetos. Este patrón es especialmente útil cuando tu código debe funcionar con diversas familias de productos o cuando hay una dependencia compleja entre los objetos que conforman estas familias.

Este patrón se utiliza para delegar la responsabilidad de la creación de objetos a una clase específica que se encarga de determinar qué productos de una familia de productos deben ser creados.

La principal ventaja de este patrón es que aísla el código de la construcción de los objetos de sus representaciones concretas. Esto significa que el mismo código puede funcionar con diferentes familias de productos, lo que aumenta la flexibilidad y la reutilización del código. Además, este patrón fomenta la cohesión entre los productos que forman una familia, asegurando que los objetos que se utilizan juntos sean compatibles entre sí.

Para entender mejor las factorías abstractas vamos a explicarlas con un ejemplo. Partiremos de dos interfaces que definen la implementación de objetos de la misma familia:

interface ProductoAInterface
{
    public function getTipo();
}

interface ProductoBInterface
{
    public function getTipo();
}

Ahora vamos a definir implementaciones para las interfaces anteriores:

class ProductoA implements ProductoAInterface
{
    public function getTipo()
    {
        return 'Tipo A';
    }
}

class ProductoB implements ProductoBInterface
{
    public function getTipo()
    {
        return 'Tipo B';
    }
}

Ahora vamos a definir una interfaz para la factoría que creará nuestros productos. Esta factoría contendrá métodos para crear ambas familias de productos:

interface FactoriaProductoInterface
{
    public function crearProductoA(): ProductoAInterface;
    public function crearProductoB(): ProductoBInterface;
}

Finalmente definiremos una implementación de la interfaz FactoriaProductoInterface:

class FactoriaProducto implements FactoriaProductoInterface
{
    public function crearProductoA(): ProductoAInterface
    {
        return new ProductoA();
    }
    public function crearProductoB(): ProductoBInterface
    {
        return new ProductoB();
    }
}

Con esto ya hemos terminado la definición. Ahora vamos a crear una instancia de la factoría:

$factoria = new FactoriaProducto();

Ahora vamos a usar la factoría que hemos creado para crear productos de ambos tipos:

$productoA = $factoria->crearProductoA();
$productoB = $factoria->crearProductoB();

Si ahora mostramos el tipo de producto, veremos que el tipo devuelto por productoA es Tipo A y el tipo devuelto por productoB es Tipo B:

echo $productoA->getTipo(); // Tipo A
echo $productoB->getTipo(); // Tipo B

La implementación de este patrón implica a los componentes clave:

  • Factoría abstracta: Se trata de una interfaz que declara un conjunto de métodos para crear cada uno de los objetos abstractos de la familia de productos.
  • Factorías concretas: Son clases que implementan la interfaz de la factoría abstracta para crear objetos concretos de los productos. Cada factoría concreta se corresponde con una específica familia de productos y crea objetos de esa familia.
  • Productos abstractos: Son interfaces o clases abstractas que definen los productos que pueden ser creados. Estas son las representaciones generales de los objetos que la factoría puede producir.
  • Productos concretos: Clases que implementan las interfaces de los productos o heredan de las clases abstractas de los productos. Estos son los objetos reales que serán utilizados por el sistema. Cada producto concreto pertenece a una única variante de una familia de productos y es creado por una fábrica concreta.

Este patrón se usa cuando una clase no puede anticipar la clase de objetos que debe crear, permitiendo así encapsular la creación de objetos en sus propias subclases, de modo que se permita que una clase delegue esa decisión a subclases.

También permite encapsular la lógica de creación de un objeto. Esto resulta muy valioso cuando el proceso de creación es complejo o si depende de operaciones o parámetros de configuración que no deben ser expuestos fuera de la fábrica.

Este patrón reduce el acoplamiento y da cierta flexibilidad al trabajar con interfaces o clases base y no con clases concretas, sin embargo, tal y como ves, se introducen muchas interfaces y subclases, aumentando considerablemente la complejidad del código.

La diferencia entre el patrón factoría estándar y el de factoría abstracta, es que, mientras que el patrón factoría se encarga de la creación de un solo producto, el patrón de factoría abstracta se encarga de gestionar la creación de familias de productos.

El patrón Constructor (Builder)

El patrón de diseño Builder es un patrón de creación de objetos que se utiliza para construir objetos complejos paso a paso. En lugar de instanciar un objeto directamente mediante su constructor, que requiere múltiples parámetros, el patrón Builder separa la construcción del objeto de su representación, permitiendo el uso del mismo proceso de construcción para crear diferentes representaciones.

Este patrón es que resulta útil cuando un objeto debe ser creado con muchas combinaciones posibles de atributos. Es decir; suele usarse cuando tus objetos tienen múltiples componentes opcionales y quieres simplificar su creación. En lugar de aumentar la cantidad de constructores, se utiliza un objeto constructor o Builder que recibe paso a paso los parámetros necesarios. Una vez que todos los parámetros han sido proporcionados, se construye el objeto deseado.

Vamos a explicarlo mejor con un ejemplo. Para empezar vamos a definir la clase Producto que contenga varias partes que pueden agregarse o listarse. Por poner un ejemplo sencillo, estas partes serán cadenas, aunque también podrían ser objetos:

class Producto
{
    private $partes = [];

    public function agregarParte($parte)
    {
        $this->partes[] = $parte;
    }

    public function listarPartes() {
        return implode(', ', $this->partes);
    }
}

Ahora vamos a definir una interfaz del Builder:

interface BuilderInterface
{
    public function buildParteA();
    public function buildParteB();
}

Ahora vamos a crear una implementación de la BuilderInterface:

class BuilderProducto implements BuilderInterface
{
    private $producto;

    public function __construct()
    {
        $this->producto = new Producto();
    }

    public function buildParteA()
    {
        $this->producto->agregarParte('Parte A');
    }

    public function buildParteB()
    {
        $this->producto->agregarParte('Parte B');
    }

    public function getProducto()
    {
        return $this->producto;
    }
}

Habitualmente, este patrón involucra la creación de un director, que gestionará la creación del producto, aunque esto podrías hacerlo directamente en un servicio:

class Director
{
    public function buildProducto(BuilderInterface $builder)
    {
        $builder->buildParteA();
        $builder->buildParteB();
    }
}

Vamos a comenzar creando un director que se encargue de crear un producto con ciertas características:

$director = new Director();

Ahora vamos a crear una instancia de un BuilderProducto:

$builder = new ConcreteBuilder();

Seguidamente, le diremos a nuestro director que nos devuelva una nueva instancia de un producto:

$director->buildProduct($builder);

En el interior del director se han agregado tanto la parte A como la parte B mediante los métodos buildParteA y buildParteB respectivamente.

Finalmente vamos a obtener el producto creado y a mostrar sus partes:

$producto = $builder->getProducto();
echo $product->listarPartes(); // Parte A, Parte B

Por poner otro ejemplo, imagina que estás construyendo una casa. El jefe de obra o director de obra dirige su construcción. El proceso de construcción podría requerir pasos como cimentación, paredes, techo, y demás partes, que serían los métodos del Builder. Dependiendo del tipo de casa que quieras construir crearás un Builder concreto u otro, ya que los detalles de estos pasos variarán, aunque el proceso general se mantenga. Al final, obtienes la casa construida según tus especificaciones, a la que podrías llamar Producto.

Los componentes principales del patrón Builder son los siguientes:

  • Director: Guía la construcción del objeto utilizando el builder. Es opcional.
  • Interfaz Builder: Interfaz que define los métodos para construir las partes de un producto complejo.
  • Builder concreto: Implementa la interfaz Builder y proporciona los métodos para construir las partes del producto. Mantiene la instancia del producto que se está construyendo.
  • Producto: El objeto complejo que está siendo construido.

Este patrón resulta útil cuando se quiere separar la construcción de los objetos de su representación, permitiendo construir objetos complejos paso a paso. También otorga encapsulamiento al proceso, ya que el cliente no necesita conocer la composición interna de los objetos complejos. Finalmente, El patrón Builder permite controlar el proceso de construcción, admitiendo variaciones más granuladas del proceso.

Patrón Decorador (Decorator)

El patrón Decorator es un patrón de diseño estructural que permite agregar nuevas funcionalidades a tus objetos de forma dinámica.  Los decoradores aceptan un objeto en su constructor de forma que no se modifique el código de las clases existentes. Este patrón resulta útil cuando se desea extender la funcionalidad de una clase sin usar herencia. Es posible usar múltiples decoradores al mismo tiempo para añadir varias capas de comportamiento sobre un objeto.

Vamos a explicar mejor su funcionamiento mediante un ejemplo, declarando una interfaz llamada BatidoInterface:

interface BatidoInterface
{
    public function precio();
}

Ahora vamos a crear una implementación de la interfaz BatidoInterface:

class Batido implements BatidoInterface
{
    public function precio()
    {
        return 10;
    }
}

Ahora vamos a crear una clase abstracta para los decoradores de los batidos, a la que llamaremos DecoradorBatido. Esta clase implementará la interfaz BatidoInterface:

abstract class DecoradorBatido implements BatidoInterface
{
    protected $batido;

    public function __construct(BatidoInterface $batido)
    {
        $this->batido= $batido;
    }
}

Ahora vamos a crear dos decoradores, uno llamado DecoradorGalletas y otro llamado DecoradorFresa:

class DecoradorGalletas extends DecoradorBatido
{
    public function precio()
    {
        return $this->batido->precio() + 4;
    }
}

class DecoradorFresa extends DecoradorBatido
{
    public function precio()
    {
        return $this->batido->precio() + 2;
    }
}

Finalmente crearemos una nueva instancia de un batido

$batido = new Batido();

Ahora crearemos una instancia del decorador DecoradorGalletas, por escoger uno, al que el pasaremos el batido como parámetro:

$batidoConGalletas = new DecoradorGalletas($batido);

Ahora mostraremos su precio:

echo $batidoConGalletas->precio(); // 14

Dado que los decoradores implementan la interfaz BatidoInterface y también aceptan elementos BatidoInterface en su constructor, podemos agregar más toppings:

$batidoConGalletasYFresa = new DecoradorFresa($batidoConGalletas);

Si ahora mostraremos su precio:

echo $batidoConGalletasYFresa->precio(); // 16

Estos son los componentes básicos del patrón Decorator:

  • Componente: Define la interfaz para los objetos que pueden tener responsabilidades añadidas dinámicamente.
  • Componente Concreto: Es la clase que implementa la interfaz del componente, representando los objetos a los cuales se les añadirán nuevas funcionalidades.
  • Decorador: Mantiene una referencia al objeto Componente y también implementa la interfaz Componente. Actúa como la clase base para todos los decoradores concretos.
  • Decoradores Concretos: Son clases que extienden la funcionalidad del Componente Concreto, añadiendo su propio comportamiento antes, después o incluso en lugar del comportamiento del componente al que decoran.

Los decoradores ofrecen una forma flexible de añadir o modificar funcionalidades de objetos en tiempo de ejecución. Los componentes y decoradores pueden reutilizarse de froma independiente, favoreciendo así la composición sobre la herencia. Esto permite extender las capacidades de clases que están cerradas a ser modificadas, como las declaradas con final, ya que no requieren cambiar el código existente.

Patrón Prototipo (Prototype)

El patrón Prototype es un patrón de diseño creacional que se utiliza para clonar objetos fácilmente. Resulta especialmente útil en escenarios donde la creación de un objeto es más costosa que el acto de copiar un objeto existente. Al permitir la clonación de objetos , el patrón Prototype evita el malgasto de los recursos asociados con la creación de nuevas instancias desde cero, especialmente cuando estas operaciones incluyen llamadas a bases de datos, llamadas a alguna API o la lectura de ciertos archivos de configuración complejos.

Vamos a explicar el patrón con un ejemplo:

Vamos a comenzar definiendo la interfaz Prototype que especificará el método clone que usaremos en nuestras implementaciones:

interface Prototype
{
    public function clone(): Prototype;
}

Ahora vamos a crear la clase Persona, con el método getNombre:

class Persona implements Prototype
{
    private $nombre;

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

    public function getNombre()
  {
        return $this->nombre;
    }

    public function setNombre($nombre)
    {
        $this->nombre= $nombre;
    }
    public function clone(): Prototype
    {
        return clone $this;
    }
}

Ahora vamos a crear una instancia de la clase Persona:

$persona = new Persona("Edu");

Finalmente vamos a crear un clon:

$clonPersona = $persona->clone();

Vamos a mostrar el nombre de ambas para comprobar que es el mismo:

echo $persona->getNombre(); // Edu
echo $clonPersona->getNombre(); // Edu

Se trata de un patrón muy sencillo, tal y como puedes comprobar. Implica los siguientes componentes:

  • Prototipo: Una interfaz que define el método de clonación.
  • Prototipo Concreto: Implementa la operación de clonación que devuelve una copia de sí mismo.

Este patrón implica ciertos beneficios. Entre ellos se encuentra la optimización del rendimiento y uso de recursos, ya que se evita la necesidad de rehacer ciertas operaciones al crear objetos idénticos. Los objetos pueden ser clonados y modificados según sea necesario, ya que el proceso de creación puede ser rígido y complejo. Además, también se reduce la necesidad de crear subclases, ya que los objetos clonados pueden diferenciarse mediante la modificación de sus propiedades una vez hayan sido clonados.

El patrón Adaptador (Adapter)

El patrón Adapter es un patrón de diseño estructural que actúa como un puente entre dos interfaces incompatibles. Permite que una interfaz de una clase existente sea usada como si fuese otra interfaz. Este patrón resulta útil cuando se necesita integrar clases nuevas con clases existentes sin modificar el código de las clases existentes.

Cuando se usa, se realiza una llamada a una operación en el adaptador, que usa la clase adaptada para completa la operación, encapsulando la misma. El adaptador traduce esa llamada a una o más llamadas, adecuadas para la clase adaptada. Finalmente, se recibe el resultado de la llamada sin saber que está interactuando con una clase adaptada.

Vamos a explicar el patrón Adapter con un ejemplo, en el que comenzamos creando la clase que debemos adaptar. Supongamos que en su interior se realiza una petición:

class Adaptada
{
    public function requestEspecifica()
{
        return 'Request específica';
    }
}

Además, tenemos una interfaz para el adaptador, a la que llamaremos AdaptadorInterface:

interface AdaptadorInterface
{
    public function request();
}

Lo que queremos es ejecutar el método request en la clase requestEspecífica, pero no está disponible. Podemos lograrlo creando una implementación de la interfaz AdaptadorInterface que acepte la clase Adaptada en su constructor, que usará para realizar la petición en su método request:

class Adaptador implements AdaptadorInterface
{
    private $adaptada;

    public function __construct(Adaptada $adaptada)
    {
        $this->adaptada = $adaptada;
    }

    public function request()
    {
        return $this->adaptada->requestEspecifica();
    }
}

Crearemos una nueva instancia de la clase Adaptada:

$adaptada = new Adaptada();

Luego, crearemos el adaptador, al que pasamos la instancia de la clase Adaptada como argumento:

$adaptador = new Adaptador($adaptada);

Tal y como ves, se muestra el mensaje Request específica:

echo $adaptador->request(); // Request específica 

Estos son los componentes principales del patrón Adapter:

  • Interfaz del adaptador: La interfaz que el cliente espera o utiliza. Define cómo se espera que se comuniquen las clases.
  • Adaptador: Implementa la interfaz del adaptador y contiene una referencia a una instancia de la clase adaptada. El adaptador traduce las llamadas a la interfaz en un formato que la clase adaptada puede entender.
  • Clase adaptada: La clase que necesita ser adaptada. Tiene una interfaz que necesita ser convertida para ser compatible con la interfaz del adaptador.

Este patrón permite que clases con interfaces incompatibles trabajen juntas, reutilizando además clases existentes, incluso si sus interfaces no coinciden con las que se necesitan. Además, los adaptadores pueden añadirse o actualizarse fácilmente para introducir compatibilidad con clases nuevas o diferentes sin cambiar el código existente.

Patrón Estrategia (Strategy)

El patrón de diseño Strategy permite definir una familia de algoritmos, encapsular cada uno de ellos como un objeto y hacerlos intercambiables. Este patrón deja que el algoritmo varíe independientemente en cada implementación, permitiendo seleccionar el algoritmo a ejecutar en tiempo de ejecución.

Mediante este patrón, es escoge una estrategia concreta de entre varias. En ocasiones se selecciona la estrategia más adecuada en tiempo de ejecución basándose en algún criterio. Cuando se ejecuta una operación, se delega la llamada a una estrategia concreta. Todas estas estrategias son métodos definidos mediante una interfaz común.

Vamos e ver un ejemplo. Comenzaremos creando la interfaz MetodoPagoInterface:

interface MetodoPagoInterface
{
    public function pagar($cantidad);
}

Ahora vamos a crear varias implementaciones de la interfaz MetodoPagoInterface, definiendo así varios métodos de pago:

class PagoTarjeta implements MetodoPagoInterface
{
    private $numeroTarjeta;

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

    public function pagar($cantidad)
    {
        echo "Se ha pagado $cantidad mediante la tarjeta $this->cardNumber.\n";
    }
}

class PagoPaypal implements MetodoPagoInterface
{
    private $email;

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

    public function pagar($cantidad)
    {
        echo "Se ha pagado $cantidad mediante la cuenta de PayPal $this->email.\n";
    }
}

Ahora crearemos instancias de los dos métodos de pago que hemos creado:

$pagoConTarjeta = new PagoTarjeta('0953-5548-0049-2445');
$pagoConPaypal = new PagoPaypal('edu@neoguias.com');

Finalmente, iniciamos el pago usando el método pagar:

$amount = 100;

$pagoConTarjeta->pagar(100);
$pagoConPaypal->pagar(100);

El patrón Strategy consta de los siguientes elementos básicos:

  • Estrategia: Es una interfaz común para todas las estrategias concretas. Define un método que cada estrategia Concreta debe implementar.
  • Estrategias Concretas: Son implementaciones distintas de la interfaz de estrategia, de forma que cada una encapsule su propio algoritmo. Las estrategias concretas proporcionan diversas formas de llevar a cabo una operación, permitiendo que el mismo proceso se realice de diferentes modos.

Este método proporciona una interfaz común para diferentes clases, permitiendo variar de estrategia con facilidad, ejecutando así diferentes algoritmos. Los algoritmos encapsulados en estrategias pueden ser reutilizados en diferentes contextos.

Esta estrategia facilita la creación de nuevas formas de llevar a cabo una tarea sin modificar el código que utiliza la estrategia. Esto sigue el principio open/closed de SOLID, ya que puedes introducir nuevas estrategias sin modificar la clase que lo utiliza., ya que el método a invocar es el mismo para todas las estrategias.

Patrón Observador (Observer)

El patrón Observer define una dependencia de uno-a-muchos entre objetos, de forma que cuando un objeto cambie su estado, todas sus dependencias sean notificadas y actualizadas automáticamente. Este patrón se utiliza para implementar sistemas distribuidos de gestión de eventos, así como en modelado de datos y en programación interactiva, en donde el cambio en el estado de un objeto puede necesitar modificar otros objetos.

Mediante este patrón, los observadores se suscriben a un sujeto para recibir actualizaciones del mismo. El sujeto mantiene un registro de sus observadores y los notifica de los diversos cambios de estado mediante la llamada a un método de actualización definido en la interfaz de observador. Cada vez que ocurra un cambio de estado en el sujeto, este recorre su lista de observadores y notifica del cambio a cada uno de ellos.

Vamos a verlo mediante un ejemplo. Comenzaremos definiendo la interfaz de usarán los observadores, a la que llamaremos ObserverInterface:

interface ObserverInterface
{
    public function actualizar($info);
}

Ahora vamos a definir un par de observadores:

class ObserverA implements ObserverInterface
{
    public function actualizar($info)
    {
        echo "Ejecutando operación A con $datos\n";
    }
}

class ObserverB implements ObserverInterface
{
    public function actualizar($info)
    {
        echo "Ejecutando operación B con $datos\n";
    }
}

El sujeto, mantiene una lista de observadores y permite tanto agregar nuevos observadores como notificar a los mismo:

class Sujeto
{
    private $observers = [];

    public function agregarObserver(ObserverInterface $observer)
    {
        $this->observers[] = $observer;
    }

    public function notificarObservers($info)
    {
        foreach ($this->observers as $observer) {
            $observer->actualizar($info);
        }
    }
}

Ahora vamos a crear dos observadores:

$observerA = new ObserverA();
$observerB = new ObserverB();

Luego los agregamos al sujeto:

$sujeto = new Sujeto();
$sujeto->agregarObserver($observerA);
$sujeto->agregarObserver($observerB);

Finalmente notificamos a los observadores con alguna actualización:

$sujeto->notifyObservers("Datos actualizados.");

Este patrón, integra los siguientes componentes:

  • Sujeto: Mantiene una lista de observadores, facilita la adición o eliminación de observadores y notifica a los observadores cuando hay un cambio en su estado.
  • Interfaz del Observador: Define una interfaz de actualización que se llama en los observadores cuando el sujeto cambia de estado.
  • Observadores: Implementan la interfaz de Observador y definen las acciones a realizar cuando el sujeto notifica un cambio.

Esten patrón favorece el desacoplamiento de clases, ya que los sujetos no necesitan saber nada acerca de sus observadores, más allá de que implementan una cierta interfaz. Además, los observadores facilitan que los cambios en un objeto se propaguen automáticamente a todos los objetos dependientes sin hacerlos dependientes unos de otros. Los observadores pueden ser agregados o eliminados en tiempo de ejecución, lo que permite modificar fácilmente cómo responden a los cambios de estado.

Patrón Fachada (Facade)

El patrón Facade proporciona una interfaz unificada a un conjunto de interfaces en un subsistema. Este patrón define una interfaz de nivel más alto que hace que el subsistema sea más fácil de usar. Se utiliza para crear un simple punto de entrada o fachada a para un sistema más complejo.

Vamos a verlo con un ejemplo en el que definiremos tres clases, que forman parte de un mismo sistema:

class Procesador
{
    public function ejecutar()
    {
        echo "Procesador: Ejecutando proceso...\n";
    }
}

class Memoria
{
    public function cargar()
    {
        echo "Memoria: Cargando datos en la memoria...\n";
    }
}

class Disco
{
    public function leer($posicion, $size)
    {
        echo "Disco Duro: Leyendo {$size} bytes desde la posición {$posicion}\n";
    }
}

Ahora vamos a implementar una Facade que nos proporcione un acceso más rápido a ciertas funciones:

class OrdenadorFacade
{
    protected Procesador $procesador;
    protected Memoria $memoria;
    protected Disco $discoDuro;

    public function __construct()
     {
        $this->procesador = new Procesador();
        $this->memoria = new Memoria();
        $this->discoDuro = new DiscoDuro();
    }

    public function arrancar()
    {
        $this->discoDuro->leer(0, 1024);
        $this->memoria->cargar();
        $this->procesador->ejecutar();
    }
}

Ahora podemos ejecutar varias operaciones en cadena con más facilidad:

$ordenador = new OrdenadorFacade();
$ordenador->arrancar();

Esta metdología resulta útil cuando quremos encapsular una librería que gestiona una API externa, de forma que, por ejemplo, nos ahorremos la parte de la conexión o nos evitemos tener que realizar varias operaciones recurrentes.

La mayor ventaja de este patrón, es que evitamos que otros desarrolladores necesiten lidiar con la complejidad subyacente, reduciendo además el número de dependencias del código en caso de usar este patrón como parte de una librería. Además, el código se encapsula, permitiendo así una organización modular del código, estando mejor organizado.

Este patrón es especialmente útil en sistemas grandes y complejos, facilitando su uso y mantenimiento al limitar la complejidad expuesta a los usuarios y otros sistemas.

Patrón Proxy

El patrón Proxy es un patrón de diseño estructural que proporciona un objeto que sustituye otro objeto para controlar así el acceso al mismo. Este patrón es útil cuando se quiere añadir una capa de control sobre la interacción con un objeto, ya sea por razones de seguridad, costes operacional o con el objetivo de añadir funcionalidades adicionales. El patrón Proxy actúa como un intermediario entre nuestro código y el objeto real, permitiendo intervenir en la llamada a métodos

Mediante este método, accedemos al Proxy en lugar de interactuar directamente con la clase final. El Proxy realiza operaciones adicionales antes o después de pasar la llamada a la clase final según sea necesario. El Proxy puede decidir no delegar la llamada a la clase final si se cumplen ciertas condiciones.

Vamos a verlo mejor mediante un ejemplo. Vamos a definir una interfaz que tanto la clase final como el proxy implementarán. Esta interfaz representa las operaciones que se pueden realizar sobre los documentos:

interface Documento
{
    public function mostrar();
}

Ahora implementamos, la clase final, que es el objeto que el proxy está protegiendo. En este caso, es un documento simple que cualquier usuario puede querer ver:

class DocumentoReal implements Documento
{
    private $nombre;

    public function __construct($nombre)
    {
        $this->nombre = $nombre;
        echo "Cargando documento $nombre...\n";
    }

    public function mostrar()
    {
        echo "Mostrando documento $nombre\n";
    }
}

Ahora, implementamos el Proxy, que controla el acceso a la clase final DocumentoReal. En este ejemplo, el proxy verifica si el usuario está autorizado antes de permitirle ver el documento:

class ProxyDocumento implements Documento
{
    private $documento;
    private $nombre;
    private $usuarioAutorizado;

    public function __construct($nombre, $usuarioAutorizado)
    {
        $this->nombre = $nombre;
        $this->usuarioAutorizado = $usuarioAutorizado;
    }

    public function mostrar()
    {
        if ($this->usuarioAutorizado) {
            if ($this->documento == null) {
                $this->documento = new DocumentoReal($this->nombre);
            }
            $this->documento->mostrar();
        } else {
            echo "Acceso denegado. Usuario no autorizado para ver el documento.\n";
        }
    }
}

Finalmente, vamos a interactuar con el Proxy en lugar de hacerlo con la clase DocumentoReal directamente. El proxy controla el acceso al documento basándose en la autorización del usuario.

function clienteCode(Documento $documento) {
    $documento->mostrar();
}

// Usuario autorizado
$documento1 = new ProxyDocumento("Documento1.pdf", true);
$documento1->mostrar(); // Mostrando documento Documento1.pdf

// Usuario no autorizado
$documento2 = new ProxyDocumento("Documento2.pdf", false);
$documento2->mostrar(); // Acceso denegado

El Patrón Proxy consta de los siguientes componentes:

  • Interfaz: Define la interfaz común tanto para la clase final como para el Proxy, permitiendo que un Proxy se use en lugar de dicha clase.
  • Clase real: La clase que define el objeto real que el Proxy representa.
  • Proxy: Mantiene una referencia que permite al proxy acceder al objeto real, controlando el acceso a este.

El proxy puede proteger el objeto real de accesos no autorizados. Como ventaja adicional, con el uso de proxies, ciertos objetos pesados solo se crearán bajo demanda, ahorrando recursos. Además, los desarrolladores pueden no ser conscientes de que están trabajando con un proxy en lugar de estar trabajando con un objeto real. Otra ventaja es la flexibilidad, ya que un Proxy no modifica la clase que utiliza.

 


Avatar de Edu Lazaro

Edu Lázaro: Ingeniero técnico en informática, actualmente trabajo como desarrollador web y programador de videojuegos.

👋 Hola! Soy Edu, me encanta crear cosas y he redactado esta guía. Si te ha resultado útil, el mayor favor que me podrías hacer es el de compatirla en Twitter 😊

Si quieres conocer mis proyectos, sígueme en Twitter.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

“- Hey, Doc. No tenemos suficiente carretera para ir a 140/h km. - ¿Carretera? A donde vamos, no necesitaremos carreteras.”