🎁 El Patrón Decorator: envuelve, apila y extiende
¿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, CafeConLecheChocolateYCanela… y así hasta el infinito, el patrón Decorator te permite envolver la taza de café original con objetos “decoradores” (Leche, Chocolate, Canela…) 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. Como las capas de una cebolla: cada capa añade algo nuevo sin modificar las de dentro.
¿Qué tipo de patrón de diseño es el Decorator?
El problema que resuelve
Para entender por qué existe el Decorator, piensa en el problema de la explosión de subclases. Si tienes un café con 5 posibles extras (leche, chocolate, canela, vainilla, caramelo), las combinaciones posibles son 2⁵ = 32 subclases. Si añades un sexto extra, se duplican a 64. Eso es inviable.
Una subclase por cada combinación: CafeConLeche, CafeConChocolate, CafeConLecheYChocolate, CafeConLecheChocolateYCanela... Explosión exponencial de clases.
Una clase base + un decorador por extra: CafeSimple, LecheDecorador, ChocolateDecorador, CanelaDecorador... Se combinan libremente apilando.
Con 5 extras, pasas de 32 subclases a 6 clases (1 base + 5 decoradores). Y puedes combinarlas como quieras en tiempo de ejecución. Eso es el poder del Decorator.
¿Qué problema principal resuelve el patrón Decorator?
¿Cuándo debería usarlo?
El patrón Decorator es ideal en estas 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. Trabaja con la misma interfaz.
- 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.
- Cuando quieres eliminar responsabilidades: Si la lógica del decorador se aplica solo temporalmente o de forma condicional a un objeto.
- Para tener múltiples funcionalidades apiladas: Permite aplicar varias capas de funcionalidad (por ejemplo, cifrado y compresión) al mismo objeto.
- En sistemas de middleware: Cada middleware “decora” la petición o respuesta antes de pasarla al siguiente.
Estructura del patrón
El Decorator se basa en la composición y en que tanto los objetos base como los decoradores implementan la misma interfaz. Estos son los participantes:
Define la interfaz que comparten el objeto base y los decoradores. El cliente trabaja con esta interfaz sin saber qué hay debajo.
El objeto original que recibe la funcionalidad añadida. La "taza de café solo". Funciona perfectamente por sí solo.
Clase abstracta que implementa la misma interfaz y envuelve un Component. Delega las llamadas al objeto envuelto por defecto.
Cada decorador concreto extiende el Base Decorator y añade su funcionalidad antes o después de delegar al objeto envuelto.
¿Qué deben compartir el objeto base y los decoradores?
Implementación: el ejemplo del café
<?php
// 1. La Interfaz Común (Component)
interface Cafe
{
public function getDescripcion(): string;
public function getCosto(): float;
}
// 2. El Objeto Base (Concrete Component)
class CafeSimple implements Cafe
{
public function getDescripcion(): string
{
return "Café Solo";
}
public function getCosto(): float
{
return 2.00;
}
}
// 3. La Clase Abstracta Decorator (Base Decorator)
abstract class DecoradorCafe implements Cafe
{
public function __construct(protected Cafe $cafe) {}
public function getDescripcion(): string
{
return $this->cafe->getDescripcion();
}
public function getCosto(): float
{
return $this->cafe->getCosto();
}
}
// 4. Decoradores Concretos
class LecheDecorador extends DecoradorCafe
{
public function getDescripcion(): string
{
return parent::getDescripcion() . ", con Leche";
}
public function getCosto(): float
{
return parent::getCosto() + 0.50;
}
}
class ChocolateDecorador extends DecoradorCafe
{
public function getDescripcion(): string
{
return parent::getDescripcion() . ", con Chocolate";
}
public function getCosto(): float
{
return parent::getCosto() + 0.75;
}
}
class CanelaDecorador extends DecoradorCafe
{
public function getDescripcion(): string
{
return parent::getDescripcion() . ", con Canela";
}
public function getCosto(): float
{
return parent::getCosto() + 0.30;
}
}
Ahora a usarlo apilando decoradores como capas:
// Café solo
$miCafe = new CafeSimple();
echo $miCafe->getDescripcion() . " → " . $miCafe->getCosto() . "€\n";
// Café Solo → 2€
// Le añadimos leche
$miCafe = new LecheDecorador($miCafe);
echo $miCafe->getDescripcion() . " → " . $miCafe->getCosto() . "€\n";
// Café Solo, con Leche → 2.50€
// Y chocolate (decorador sobre decorador)
$miCafe = new ChocolateDecorador($miCafe);
echo $miCafe->getDescripcion() . " → " . $miCafe->getCosto() . "€\n";
// Café Solo, con Leche, con Chocolate → 3.25€
// Y canela (otro decorador más)
$miCafe = new CanelaDecorador($miCafe);
echo $miCafe->getDescripcion() . " → " . $miCafe->getCosto() . "€\n";
// Café Solo, con Leche, con Chocolate, con Canela → 3.55€
Fíjate en lo potente que es: cada línea envuelve el objeto anterior en una capa nueva. Y en ningún momento se modifica CafeSimple, ni LecheDecorador, ni ninguna otra clase. Abierto a extensión, cerrado a modificación (OCP de SOLID).
En el ejemplo del café, ¿cuántas clases necesitas para 5 extras con Decorator vs sin él?
Ejemplo práctico: procesamiento de datos
En este ejemplo más real, aplicamos diferentes capas de procesamiento (compresión y cifrado) a un objeto de datos base:
<?php
interface ProcesadorDatos
{
public function procesar(string $data): string;
}
class DatosBase implements ProcesadorDatos
{
public function procesar(string $data): string
{
return "Datos Crudos: " . $data;
}
}
abstract class DecoradorProcesamiento implements ProcesadorDatos
{
public function __construct(protected ProcesadorDatos $envoltorio) {}
public function procesar(string $data): string
{
return $this->envoltorio->procesar($data);
}
}
class CifradoDecorador extends DecoradorProcesamiento
{
public function procesar(string $data): string
{
$datosCifrados = "[CIFRADO](" . $data . ")";
return $this->envoltorio->procesar($datosCifrados);
}
}
class CompresionDecorador extends DecoradorProcesamiento
{
public function procesar(string $data): string
{
$datosComprimidos = "[COMPRIMIDO](" . $data . ")";
return $this->envoltorio->procesar($datosComprimidos);
}
}
class LogDecorador extends DecoradorProcesamiento
{
public function procesar(string $data): string
{
echo "[LOG] Procesando datos de " . strlen($data) . " bytes\n";
return $this->envoltorio->procesar($data);
}
}
Ahora puedes combinar los decoradores en el orden que necesites:
$datos = "Información importante y confidencial.";
// Solo datos base
$base = new DatosBase();
echo $base->procesar($datos) . "\n";
// Datos Crudos: Información importante y confidencial.
// Cifrado + Base
$cifrado = new CifradoDecorador(new DatosBase());
echo $cifrado->procesar($datos) . "\n";
// Datos Crudos: [CIFRADO](Información importante y confidencial.)
// Log + Cifrado + Compresión + Base
$completo = new LogDecorador(
new CifradoDecorador(
new CompresionDecorador(
new DatosBase()
)
)
);
echo $completo->procesar($datos) . "\n";
// [LOG] Procesando datos de 38 bytes
// Datos Crudos: [COMPRIMIDO]([CIFRADO](Información importante y confidencial.))
Fíjate que el orden importa. Primero se cifra, luego se comprime, luego se loguea. Cambiar el orden cambia el resultado. Es como las capas de la cebolla: se aplican de fuera hacia dentro.
¿El orden de los decoradores importa?
Ejemplo del mundo real: middleware HTTP
Si has trabajado con frameworks como Laravel, Symfony o Express, ya has usado el patrón Decorator sin saberlo. El sistema de middleware es un Decorator en toda regla:
<?php
interface HttpHandler
{
public function handle(Request $request): Response;
}
class AppController implements HttpHandler
{
public function handle(Request $request): Response
{
return new Response("Respuesta de la aplicación");
}
}
abstract class Middleware implements HttpHandler
{
public function __construct(protected HttpHandler $next) {}
}
class AuthMiddleware extends Middleware
{
public function handle(Request $request): Response
{
if (!$request->hasValidToken()) {
return new Response("No autorizado", 401);
}
// Si tiene token, pasa al siguiente
return $this->next->handle($request);
}
}
class LogMiddleware extends Middleware
{
public function handle(Request $request): Response
{
$start = microtime(true);
$response = $this->next->handle($request);
$duration = microtime(true) - $start;
echo "[LOG] {$request->getMethod()} {$request->getPath()} - {$duration}s\n";
return $response;
}
}
class CorsMiddleware extends Middleware
{
public function handle(Request $request): Response
{
$response = $this->next->handle($request);
$response->addHeader("Access-Control-Allow-Origin", "*");
return $response;
}
}
// Apilar middlewares (el orden define el pipeline)
$app = new LogMiddleware(
new AuthMiddleware(
new CorsMiddleware(
new AppController()
)
)
);
$response = $app->handle($request);
Cada middleware es un decorador que envuelve al siguiente. La petición pasa por Log → Auth → CORS → Controller, y la respuesta vuelve en el sentido contrario. Si el Auth falla, ni siquiera llega al controlador. Esto es el Decorator aplicado a infraestructura real.
¿Qué sistema de los frameworks web es un ejemplo real del patrón Decorator?
Decorator en el día a día: dónde lo vas a encontrar
El patrón Decorator está por todas partes aunque no lo veas:
En Java/PHP, los streams de I/O son decoradores puros: BufferedReader(FileReader(file)). Cada capa añade funcionalidad (buffer, encoding...).
Laravel, Symfony, Express... Los middlewares son decoradores que envuelven la petición/respuesta añadiendo auth, logs, CORS, rate limiting...
Un CachedRepository que envuelve un Repository: si está en caché, lo devuelve; si no, llama al repositorio real. El cliente no se entera.
Envolver cualquier servicio con un decorador de logging para registrar todas las llamadas sin tocar el servicio original.
Un decorador que valida los datos de entrada antes de pasarlos al servicio real. Si la validación falla, ni siquiera llega al servicio.
Un decorador que cuenta las peticiones por usuario y las bloquea si superan el límite. El servicio decorado no sabe nada de esto.
Relación con los principios SOLID
El Decorator es un patrón que nace naturalmente cuando aplicas SOLID:
- SRP: Cada decorador tiene una sola responsabilidad (solo cifrado, solo compresión, solo logging…).
- OCP: Puedes añadir funcionalidades nuevas creando decoradores nuevos sin modificar el objeto base ni los decoradores existentes.
- LSP: Los decoradores implementan la misma interfaz que el componente, así que son intercambiables.
- ISP: La interfaz del componente debe ser pequeña y enfocada para que los decoradores no tengan que implementar métodos innecesarios.
- DIP: El cliente depende de la interfaz (abstracción), no de la implementación concreta. No sabe si el objeto está decorado o no.
¿Qué principio SOLID cumple el Decorator al crear nuevos decoradores sin tocar los existentes?
Ventajas y desventajas
- Flexibilidad: Añade o elimina responsabilidades en tiempo de ejecución.
- Evita la explosión de subclases: N decoradores cubren 2^N combinaciones.
- SRP: Cada decorador se centra en una única funcionalidad.
- OCP: Nuevas funcionalidades = nuevos decoradores, sin tocar lo existente.
- Transparencia: Misma interfaz, el cliente no nota la diferencia.
- Muchos objetos pequeños: Puede resultar en muchas clases que solo delegan.
- Configuración compleja: Envolver muchas veces complica la instanciación y el debugging.
- El orden importa: Apilar decoradores en orden incorrecto puede dar resultados inesperados.
- Identidad del objeto: Se pierde la clase concreta del objeto envuelto (solo se conoce la interfaz).
Decorator vs otros patrones
Es fácil confundir el Decorator con otros patrones, aquí van las diferencias clave:
| Patrón | Propósito | Diferencia con Decorator |
|---|---|---|
| Adapter | Cambia la interfaz de un objeto | Decorator no cambia la interfaz, la mantiene igual |
| Proxy | Controla el acceso al objeto | Decorator añade funcionalidad, no controla acceso |
| Strategy | Intercambia algoritmos completos | Decorator apila capas incrementales, no reemplaza |
| Chain of Resp. | Pasa la petición hasta que alguien la maneje | Decorator siempre ejecuta todos los eslabones de la cadena |
¿Cuál es la diferencia principal entre Decorator y Adapter?
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 brutal 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 la “explosión de subclases” y te lo vas a encontrar en middleware, caché, logging, streams de I/O y muchos más sitios.
La clave: misma interfaz, funcionalidad apilable, objeto original intacto.
¡Un saludo y nos vemos en los bares! 🍻