El patrón Decorator permite extender el comportamiento de un objeto envolviéndolo en otra clase que implementa la misma interfaz, sin alterar su código fuente. Esta técnica ofrece varias ventajas clave:
- Acoplamiento reducido: El objeto envolvente y el envuelto pueden evolucionar de manera independiente.
- Composición flexible: Es posible encadenar múltiples decoradores en tiempo de ejecución para construir distintas variantes de funcionalidad.
- Evita jerarquías de herencia complejas: Al no depender de subclases, se mantiene el sistema más limpio y manejable.
- Reutilización: Cada decorador encapsula una única responsabilidad y puede aplicarse a cualquier objeto que comparta la misma interfaz.
- Cumplimiento del principio de abierto/cerrado: Se añaden capacidades sin modificar el código existente.
Conceptos fundamentales
- Interfaz componente: Define el contrato común que comparten tanto el objeto original como los decoradores.
- Componente concreto: Implementa la interfaz y representa el objeto base que se desea envolver.
- Clase decoradora base: Implementa la interfaz y mantiene una referencia al objeto envuelto, delegando las llamadas en él.
- Decoradores concretos: Heredan de la base y añaden lógica adicinoal antes o después de delegar la ejecución.
La interfaz y el componente concreto pueden provenir de una biblioteca externa o de código propio. El decorador actúa como intermediario: recibe las mismas llamadas que el objeto original, puede procesarlas y luego las reenvía al componente interno.
Ejemplo práctico: envolver una clase de transacciones
Supongamos que existe una clase externa con métodos para comprar y vender. Aplicaremos el patrón Decorator para añadir capacidades como registro de logs sin modificar dicha clase.
Primero, declaramos la interfaz común:
class ITransactionOps {
public:
virtual ~ITransactionOps() = default;
virtual void executePurchase() = 0;
virtual void executeSale() = 0;
};
Luego, implementamos el componente concreto:
class StandardTransaction : public ITransactionOps {
public:
void executePurchase() override {
// Lógica de compra
}
void executeSale() override {
// Lógica de venta
}
};
Definimos la clase decoradora base que delega al objeto interno:
class TransactionWrapper : public ITransactionOps {
protected:
ITransactionOps* inner;
public:
TransactionWrapper(ITransactionOps* target) : inner(target) {}
void executePurchase() override {
inner->executePurchase();
}
void executeSale() override {
inner->executeSale();
}
};
Finalmente, creamos un decorador concreto que añade registro de actividad:
class AuditLogWrapper : public TransactionWrapper {
public:
AuditLogWrapper(ITransactionOps* target) : TransactionWrapper(target) {}
void executePurchase() override {
// Registrar evento de compra
TransactionWrapper::executePurchase();
}
void executeSale() override {
// Registrar evento de venta
TransactionWrapper::executeSale();
}
};
Uso en el programa principal:
int main() {
ITransactionOps* baseTx = new StandardTransaction();
ITransactionOps* loggedTx = new AuditLogWrapper(baseTx);
loggedTx->executePurchase();
loggedTx->executeSale();
delete loggedTx;
delete baseTx;
return 0;
}