El patrón Decorator permite agregar responsabilidades a un objeto de forma dinámica, envolviéndolo en otra clase que implementa la misma interfaz. A diferencia de la herencia clásica, donde las extensiones son estáticas y se conocen en tiempo de compilación, Decorator habilita combinaciones flexibles en tiempo de ejecución.
El patrón se utiliza cuando se desea extender el comportamiento de un objeto sin modificar su código ni afectar a otros objetos de la misma clase. Esto resulta ideal para introducir funcionalidades transversales como trazas, validaciones, cifrado o compresión.
En situaciones donde se crean jerarquías extensas de clases para representar cada combinación de funcionalidades, el mantenimiento se vuelve costoso. Cada nueva capacidad requiere heredar de todas las variantes existentes o duplicar código. Decorator evita la explosión de subclasses envolviendo objetos con decoradores especializados.
También permite seguir el principio de responsabilidad única: cada decorador implementa una funcionalidad concreta, y los clientes pueden componer varios decoradores para lograr comportamientos complejos.
Su intención es adjuntar responsabilidades adicionales a un objeto de manera flexible. Al envolver el componente original dentro de uno o varios decoradores, se logra ampliar su comportamiento sin modificar la implementación de base.
El patrón es común en bibliotecas de interfaz gráfica, donde controles visuales reciben adornos como bordes, barras de desplazamiento o sombras. También se utiliza para enriquecer flujos de entrada/salida, tal como hace la biblioteca estándar de .NET con los streams.
Los elementos clave del patrón son:
La estructura es recursiva: un decorador puede envolver a otro, lo que permite apilar responsabilidades sin límite teórico.
Analicemos un ejemplo en C# donde un servicio de texto puede enriquecerse con validación y compresión antes de almacenar los datos.
using System;
public interface IAlmacenamiento
{
void Guardar(string datos);
}
public class AlmacenamientoArchivo : IAlmacenamiento
{
public void Guardar(string datos)
{
Console.WriteLine($"Guardando en archivo: {datos}");
}
}
public abstract class AlmacenamientoDecorator : IAlmacenamiento
{
protected readonly IAlmacenamiento Wrappee;
protected AlmacenamientoDecorator(IAlmacenamiento wrappee)
{
Wrappee = wrappee;
}
public abstract void Guardar(string datos);
}
public class AlmacenamientoValidado : AlmacenamientoDecorator
{
public AlmacenamientoValidado(IAlmacenamiento wrappee) : base(wrappee)
{
}
public override void Guardar(string datos)
{
if (string.IsNullOrWhiteSpace(datos))
{
throw new ArgumentException("Los datos no pueden estar vacios", nameof(datos));
}
Wrappee.Guardar(datos);
}
}
public class AlmacenamientoComprimido : AlmacenamientoDecorator
{
public AlmacenamientoComprimido(IAlmacenamiento wrappee) : base(wrappee)
{
}
public override void Guardar(string datos)
{
var comprimido = $"zip({datos})";
Wrappee.Guardar(comprimido);
}
}
Los decoradores se pueden apilar según las necesidades del cliente:
using System;
public static class DemoAlmacenamiento
{
public static void Main()
{
IAlmacenamiento almacenamiento = new AlmacenamientoComprimido(
new AlmacenamientoValidado(new AlmacenamientoArchivo()));
almacenamiento.Guardar("Informe mensual");
}
}
Consideremos una biblioteca que publica mensajes hacia diferentes destinos (archivo, consola, red). Queremos permitir que los mensajes se enriquezcan con timestamps, niveles y cifrado sin explotar la jerarquía de clases. Decorator ofrece una solución modular.
using System;
public interface IEmisor
{
void Emitir(string mensaje);
}
public class EmisorConsola : IEmisor
{
public void Emitir(string mensaje)
{
Console.WriteLine(mensaje);
}
}
public abstract class EmisorDecorator : IEmisor
{
protected readonly IEmisor Wrappee;
protected EmisorDecorator(IEmisor wrappee)
{
Wrappee = wrappee;
}
public abstract void Emitir(string mensaje);
}
public class EmisorConNivel : EmisorDecorator
{
private readonly string _nivel;
public EmisorConNivel(IEmisor wrappee, string nivel) : base(wrappee)
{
_nivel = nivel;
}
public override void Emitir(string mensaje)
{
Wrappee.Emitir($"[{_nivel}] {mensaje}");
}
}
public class EmisorConTimestamp : EmisorDecorator
{
public EmisorConTimestamp(IEmisor wrappee) : base(wrappee)
{
}
public override void Emitir(string mensaje)
{
Wrappee.Emitir($"{DateTimeOffset.UtcNow:o} - {mensaje}");
}
}
public class EmisorCifrado : EmisorDecorator
{
public EmisorCifrado(IEmisor wrappee) : base(wrappee)
{
}
public override void Emitir(string mensaje)
{
var cifrado = Cifrar(mensaje);
Wrappee.Emitir(cifrado);
}
private static string Cifrar(string mensaje)
{
var chars = mensaje.ToCharArray();
Array.Reverse(chars);
return new string(chars);
}
}
El cliente decide qué combinación de decoradores utilizar:
using System;
public static class Logger
{
public static void Main()
{
IEmisor emisor = new EmisorCifrado(
new EmisorConTimestamp(
new EmisorConNivel(new EmisorConsola(), "INFO")));
emisor.Emitir("Sistema inicializado");
}
}
Agregar un nuevo adorno (por ejemplo, compresión o color en consola) solo requiere implementar otro decorador sin tocar los existentes.
El uso excesivo de decoradores puede dificultar el seguimiento de la ejecución, ya que el flujo de llamadas se distribuye entre varias clases. Además, cambiar el orden de los decoradores puede alterar el resultado, por lo que es esencial definir contratos claros.
Un error habitual es utilizar Decorator solo para delegar llamadas sin agregar comportamiento real, lo cual introduce complejidad innecesaria. Cuando la extensión es simple y estática, la herencia o una función auxiliar pueden ser suficientes.
Este patrón es recomendable cuando:
Decorator se complementa con Composite (decoradores pueden componer objetos compuestos) y Strategy (decoradores pueden inyectarse como estrategias que agregan capacidades en tiempo de ejecución).