12. Decorator (Decorador) - Patrón Estructural

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.

12.1 Problema que resuelve

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.

12.2 Intención y motivación

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 Java con los streams.

12.3 Estructura y participantes

Los elementos clave del patrón son:

  • Component: interfaz o clase abstracta que define las operaciones que los clientes esperan.
  • Concrete Component: implementación base que se decora.
  • Decorator: clase abstracta que mantiene una referencia al componente y delega las operaciones.
  • Concrete Decorators: extensiones que agregan comportamientos antes o después de delegar en el componente.

La estructura es recursiva: un decorador puede envolver a otro, lo que permite apilar responsabilidades sin límite teórico.

12.4 Implementación básica en Java

Analicemos un ejemplo en Java donde un servicio de texto puede enriquecerse con validación y compresión antes de almacenar los datos.

public interface Almacenamiento {
    void guardar(String datos);
}

public class AlmacenamientoArchivo implements Almacenamiento {
    @Override
    public void guardar(String datos) {
        System.out.println("Guardando en archivo: " + datos);
    }
}

public abstract class AlmacenamientoDecorator implements Almacenamiento {
    protected final Almacenamiento wrappee;

    protected AlmacenamientoDecorator(Almacenamiento wrappee) {
        this.wrappee = wrappee;
    }
}

public class AlmacenamientoValidado extends AlmacenamientoDecorator {
    public AlmacenamientoValidado(Almacenamiento wrappee) {
        super(wrappee);
    }

    @Override
    public void guardar(String datos) {
        if (datos == null || datos.isBlank()) {
            throw new IllegalArgumentException("Los datos no pueden estar vacĂ­os");
        }
        wrappee.guardar(datos);
    }
}

public class AlmacenamientoComprimido extends AlmacenamientoDecorator {
    public AlmacenamientoComprimido(Almacenamiento wrappee) {
        super(wrappee);
    }

    @Override
    public void guardar(String datos) {
        String comprimido = "zip(" + datos + ")";
        wrappee.guardar(comprimido);
    }
}

Los decoradores se pueden apilar según las necesidades del cliente:

public class Aplicacion {
    public static void main(String[] args) {
        Almacenamiento almacenamiento = new AlmacenamientoComprimido(
                new AlmacenamientoValidado(new AlmacenamientoArchivo()));

        almacenamiento.guardar("Informe mensual");
    }
}

12.5 Ejemplo completo en Java: canal de salida con adornos

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.

public interface Emisor {
    void emitir(String mensaje);
}

public class EmisorConsola implements Emisor {
    @Override
    public void emitir(String mensaje) {
        System.out.println(mensaje);
    }
}

public abstract class EmisorDecorator implements Emisor {
    protected final Emisor wrappee;

    protected EmisorDecorator(Emisor wrappee) {
        this.wrappee = wrappee;
    }
}

public class EmisorConNivel extends EmisorDecorator {
    private final String nivel;

    public EmisorConNivel(Emisor wrappee, String nivel) {
        super(wrappee);
        this.nivel = nivel;
    }

    @Override
    public void emitir(String mensaje) {
        wrappee.emitir("[" + nivel + "] " + mensaje);
    }
}

public class EmisorConTimestamp extends EmisorDecorator {
    public EmisorConTimestamp(Emisor wrappee) {
        super(wrappee);
    }

    @Override
    public void emitir(String mensaje) {
        wrappee.emitir(java.time.Instant.now() + " - " + mensaje);
    }
}

public class EmisorCifrado extends EmisorDecorator {
    public EmisorCifrado(Emisor wrappee) {
        super(wrappee);
    }

    @Override
    public void emitir(String mensaje) {
        String cifrado = cifrar(mensaje);
        wrappee.emitir(cifrado);
    }

    private String cifrar(String mensaje) {
        return new StringBuilder(mensaje).reverse().toString();
    }
}

El cliente decide qué combinación de decoradores utilizar:

public class Logger {
    public static void main(String[] args) {
        Emisor 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.

12.6 Buenas prácticas

  • Proveer un componente base claro que represente la funcionalidad principal.
  • Mantener los decoradores pequeños y con una responsabilidad específica para evitar duplicidad.
  • Documentar el orden recomendado de envoltura cuando el resultado depende de la secuencia de decoradores.
  • Combinar Decorator con Factory Method o Builder para construir configuraciones complejas de manera controlada.

12.7 Riesgos y anti-patrones

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.

12.8 Cuándo elegir Decorator

Este patrón es recomendable cuando:

  • Se quiere agregar funcionalidad a objetos individuales sin afectar a otros de la misma clase.
  • Se requieren combinaciones flexibles de responsabilidades adicionales.
  • Se busca evitar jerarquías de herencia profundas y poco manejables.
  • Se desea aplicar comportamientos transversales (logging, seguridad, cacheo) de forma modular.

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).