🎁 Patrón de diseño: Decorator
¿Qué es?
El patrón Decorator es un patrón de diseño del tipo estructural que permite añadir dinámicamente responsabilidades adicionales a un objeto que ya existe y está funcionando. Es una alternativa flexible a la herencia para extender la funcionalidad.
Imagina que tienes un objeto base, como una taza de café solo. En lugar de crear subclases como CafeConLeche, CafeConChocolate, CafeConLecheYChocolate, y así sucesivamente, el patrón Decorator te permite envolver la taza de café original con objetos “decoradores” (Leche, Chocolate, etc.) que añaden la nueva funcionalidad.
Cada decorador envuelve al objeto original y/o a otros decoradores, agregando un comportamiento antes o después de llamar a la funcionalidad del objeto envuelto.
¿Cuándo debería usarlo?
El patrón de diseño Decorator es ideal en las siguientes situaciones:
- Para añadir responsabilidades de forma dinámica y transparente: El cliente no debería darse cuenta de que el objeto ha sido envuelto.
- Cuando la extensión por herencia es impráctica: Si la herencia lleva a una “explosión de subclases” para cubrir todas las combinaciones de funcionalidades (el clásico problema del café con múltiples ingredientes).
- Cuando quieres eliminar responsabilidades: Si la lógica del decorador se aplica solo temporalmente a un objeto.
- Para tener múltiples funcionalidades apiladas: Permite aplicar varias capas de funcionalidad (por ejemplo, cifrado y compresión) al mismo objeto.
Implementación básica en PHP
El Decorator se basa en la composición y en el principio de que los objetos base y los decoradores implementan la misma interfaz (o extienden la misma clase abstracta).
<?php
// 1. La Interfaz Común (Componente)
interface Cafe
{
public function getDescripcion(): string;
public function getCosto(): float;
}
// 2. El Objeto Base (Componente Concreto)
class CafeSimple implements Cafe
{
public function getDescripcion(): string
{
return "Café Solo";
}
public function getCosto(): float
{
return 2.00;
}
}
// 3. La Clase Abstracta Decorator (Decorator)
abstract class DecoradorCafe implements Cafe
{
public function __construct(protected Cafe $cafe) {}
// Las llamadas se delegan al objeto envuelto por defecto
public function getDescripcion(): string
{
return $this->cafe->getDescripcion();
}
public function getCosto(): float
{
return $this->cafe->getCosto();
}
}
// 4. Decorador Concreto: Leche (Añade funcionalidad)
class LecheDecorador extends DecoradorCafe
{
public function getDescripcion(): string
{
// Añade su propia descripción
return parent::getDescripcion() . ", con Leche";
}
public function getCosto(): float
{
// Añade su propio costo
return parent::getCosto() + 0.50;
}
}
// 5. Decorador Concreto: Chocolate (Añade funcionalidad)
class ChocolateDecorador extends DecoradorCafe
{
public function getDescripcion(): string
{
return parent::getDescripcion() . ", con Chocolate";
}
public function getCosto(): float
{
return parent::getCosto() + 0.75;
}
}
// Uso del patrón: Apilando funcionalidades
$miCafe = new CafeSimple();
echo "== Cafe Base ==\n";
echo "Descripción: " . $miCafe->getDescripcion() . "\n";
echo "Costo: " . $miCafe->getCosto() . "€\n\n";
// Lo envolvemos en Leche
$miCafe = new LecheDecorador($miCafe);
echo "== Cafe con Leche ==\n";
echo "Descripción: " . $miCafe->getDescripcion() . "\n";
echo "Costo: " . $miCafe->getCosto() . "€\n\n";
// Lo envolvemos en Chocolate (decorador sobre decorador)
$miCafe = new ChocolateDecorador($miCafe);
echo "== Moka (Leche + Chocolate) ==\n";
echo "Descripción: " . $miCafe->getDescripcion() . "\n";
echo "Costo: " . $miCafe->getCosto() . "€\n";
Ejemplo práctico: Procesamiento de Datos
En este ejemplo, aplicamos diferentes capas de procesamiento (compresión y cifrado) a un objeto de datos base.
<?php
// Interfaz Componente: Define la operación
interface ProcesadorDatos {
public function procesar(string $data): string;
}
// Componente Concreto: Datos simples
class DatosBase implements ProcesadorDatos {
public function procesar(string $data): string {
return "Datos Crudos: " . $data;
}
}
// Clase Abstracta Decorator
abstract class DecoradorProcesamiento implements ProcesadorDatos {
protected ProcesadorDatos $envoltorio;
public function __construct(ProcesadorDatos $envoltorio) {
$this->envoltorio = $envoltorio;
}
public function procesar(string $data): string {
return $this->envoltorio->procesar($data); // Delega la llamada
}
}
// Decorador Concreto: Cifrado
class CifradoDecorador extends DecoradorProcesamiento {
public function procesar(string $data): string {
// Aplica el cifrado ANTES de delegar
$datosCifrados = "🔒(" . $data . ")🔒";
return $this->envoltorio->procesar($datosCifrados);
}
}
// Decorador Concreto: Compresión
class CompresionDecorador extends DecoradorProcesamiento {
public function procesar(string $data): string {
// Aplica la compresión ANTES de delegar
$datosComprimidos = "📦[" . $data . "]📦";
return $this->envoltorio->procesar($datosComprimidos);
}
}
// Uso del sistema
$datos = "Hola mundo, esto son datos importantes.";
// Base sin decoración
$base = new DatosBase();
echo "1. Sin Decorar: " . $base->procesar($datos) . "\n";
// Decorar con Cifrado
$cifrado = new CifradoDecorador(new DatosBase());
echo "2. Solo Cifrado: " . $cifrado->procesar($datos) . "\n";
// Decorar con Cifrado Y Compresión (orden importa)
$cifradoComprimido = new CompresionDecorador(
new CifradoDecorador(
new DatosBase()
)
);
echo "3. Cifrado + Compresión: " . $cifradoComprimido->procesar($datos) . "\n";
// Salida:
// 1. Sin Decorar: Datos Crudos: Hola mundo, esto son datos importantes.
// 2. Solo Cifrado: Datos Crudos: 🔒(Hola mundo, esto son datos importantes.)🔒
// 3. Cifrado + Compresión: Datos Crudos: 📦[🔒(Hola mundo, esto son datos importantes.)🔒]📦
Ventajas y desventajas
Ventajas:
- Flexibilidad: Añade o elimina responsabilidades en tiempo de ejecución.
- Evita la explosión de subclases: Se eliminan las complejas jerarquías de herencia.
- Principio de Responsabilidad Única (SRP): Cada decorador se centra en una única funcionalidad (ej. solo cifrado, solo compresión).
- Transparencia: El objeto base y el decorador tienen la misma interfaz, haciendo el cambio transparente para el cliente.
Desventajas:
- Múltiples objetos pequeños: Puede resultar en muchas clases pequeñas que solo delegan.
- Configuración compleja: Envolver un objeto muchas veces puede complicar el proceso de instanciación y depuración.
- Identidad del objeto: Se pierde la “identidad” de la clase concreta del objeto envuelto, ya que solo se conoce su interfaz (aunque esto puede ser una ventaja).
Conclusión
El patrón Decorator es una herramienta estructural elegante para extender funcionalidades sin recurrir a la herencia. Al usar la composición y la delegación, ofrece una flexibilidad superior para combinar y apilar comportamientos.
Úsalo cuando te enfrentes a un problema donde una subclase por cada variante o combinación de características resulte en una jerarquía de clases inmanejable. Es la solución ideal para el problema de “la explosión de subclases”.
¡Un saludo y nos vemos en los bares! 🍻