La carta de La Bella Napoli quedó modelada como un árbol, pero el cliente no pide "la Margarita de la carta": pide su Margarita — con doble queso, sin gluten, en ración grande. Cada extra modifica el precio y la descripción, los extras se combinan libremente y mañana habrá extras nuevos. Modelarlo con subclases es condenarse a la explosión combinatoria; modelarlo con banderas booleanas es condenar a la clase a crecer sin fin. Decorator ofrece la tercera vía: envolver el objeto con capas que añaden responsabilidades, componibles en ejecución, sin tocar ni heredar la clase original. Y de regalo veremos que el mismo truco resuelve un problema técnico pendiente del módulo 2: añadir reintentos y logging a los notificadores.

Contenido

  1. El problema en PideYa: los extras del plato
  2. Por qué la herencia no puede con esto
  3. Intención y estructura del patrón
  4. Implementación Java completa
  5. Decoradores técnicos: reintentos y logging sobre Notificador
  6. Decorator en el JDK: los streams de java.io
  7. Cuándo usarlo y cuándo no
  8. Errores comunes, ejercicios y conclusión

El problema en PideYa: los extras del plato

Cuando un cliente añade una Margarita a su pedido, puede personalizarla:

Extra Efecto en el precio Efecto en la descripción
Doble queso +1,50 € "... con doble queso"
Adaptación sin gluten +2,00 € "... (sin gluten)"
Ración grande +30% sobre el acumulado "... ración grande"

Los extras se combinan en cualquier subconjunto (y el "+30%" hace que incluso importe el orden de aplicación). Lo que viaja en cada LineaPedido del Pedido que construimos en el módulo 2 debe responder dos preguntas: ¿qué descripción imprimo en el ticket? ¿qué precio cobro? Es decir, en el dominio del pedido un plato personalizado es un Producto:

public interface Producto {
    String getDescripcion();
    BigDecimal getPrecio();
}

La Plato de la carta de la lección anterior implementa Producto trivialmente (su nombre, su precio base). El problema son las combinaciones.

Por qué la herencia no puede con esto

Intento 1: subclases. MargaritaDobleQueso, MargaritaSinGluten, MargaritaDobleQuesoSinGluten, MargaritaDobleQuesoSinGlutenGrande... Con 3 extras opcionales hay 2³ = 8 combinaciones por plato; con 5 extras, 32. Y todo se duplica para la Prosciutto, la Carbonara... La herencia fija las combinaciones en compilación, cuando los extras se deciden en ejecución, cliente a cliente. Es la explosión multiplicativa que ya olimos en Bridge, agravada: allí eran dos ejes; aquí, un eje por cada extra.

Intento 2: banderas en la clase. Plato con boolean dobleQueso, sinGluten, racionGrande y un getPrecio() lleno de if. Funciona... hasta que marketing inventa el extra "bacon" y hay que modificar Plato (adiós OCP), y la clase acumula responsabilidades de todos los extras presentes y futuros (adiós SRP). Además cada plato carga con campos que el 90% de los pedidos no usa.

La necesidad, formulada con precisión: añadir responsabilidades (precio extra, texto extra) a objetos individuales, no a clases, en cualquier combinación, decidida en tiempo de ejecución, y sin modificar el código existente.

Intención y estructura del patrón

Intención (GoF): asignar responsabilidades adicionales a un objeto dinámicamente. Los decoradores ofrecen una alternativa flexible a la herencia para extender funcionalidad.

El mecanismo tiene una simetría preciosa: el decorador implementa la misma interfaz que decora y contiene un objeto de esa interfaz. Es a la vez un Producto (hacia fuera) y un usuario de Producto (hacia dentro). Por eso los decoradores se apilan: cada capa envuelve a la anterior sin saber si envuelve al objeto base o a otra capa.

classDiagram
    class Producto {
        <<interface>>
        +getDescripcion() String
        +getPrecio() BigDecimal
    }
    class Plato {
        +getDescripcion() String
        +getPrecio() BigDecimal
    }
    class ExtraPlato {
        <<abstract>>
        #decorado: Producto
        +ExtraPlato(decorado: Producto)
        +getDescripcion() String
        +getPrecio() BigDecimal
    }
    class ExtraDobleQueso {
        +getDescripcion() String
        +getPrecio() BigDecimal
    }
    class AdaptacionSinGluten {
        +getDescripcion() String
        +getPrecio() BigDecimal
    }
    class ExtraRacionGrande {
        +getDescripcion() String
        +getPrecio() BigDecimal
    }

    Producto <|.. Plato
    Producto <|.. ExtraPlato
    ExtraPlato <|-- ExtraDobleQueso
    ExtraPlato <|-- AdaptacionSinGluten
    ExtraPlato <|-- ExtraRacionGrande
    ExtraPlato o-- Producto : envuelve
Rol GoF En PideYa
Component (interfaz común) Producto
ConcreteComponent (el objeto base) Plato
Decorator (base abstracta con la referencia) ExtraPlato
ConcreteDecorator ExtraDobleQueso, AdaptacionSinGluten, ExtraRacionGrande

Fíjate en las dos flechas de ExtraPlato: implementa Producto y contiene un Producto. Esa doble relación con la misma interfaz es la firma visual del patrón (compárala con Composite: allí el grupo contenía muchos Components; el decorador contiene exactamente uno — el GoF llama al Decorator "un composite degenerado con un solo hijo").

Implementación Java completa

El componente concreto ya lo tenemos: Plato de la carta, que implementa Producto devolviendo su nombre y su precio base.

El decorador base concentra la fontanería (guardar la referencia, delegar por defecto):

public abstract class ExtraPlato implements Producto {

    protected final Producto decorado;

    protected ExtraPlato(Producto decorado) {
        this.decorado = Objects.requireNonNull(decorado);
    }

    // Por defecto, delegar: cada decorador concreto sobrescribe lo que altera.
    @Override public String getDescripcion() { return decorado.getDescripcion(); }
    @Override public BigDecimal getPrecio() { return decorado.getPrecio(); }
}

Los decoradores concretos — cada uno es minúsculo, hace una sola cosa (SRP) y llama siempre al envuelto:

public class ExtraDobleQueso extends ExtraPlato {

    private static final BigDecimal RECARGO = new BigDecimal("1.50");

    public ExtraDobleQueso(Producto decorado) { super(decorado); }

    @Override
    public String getDescripcion() { return decorado.getDescripcion() + " con doble queso"; }

    @Override
    public BigDecimal getPrecio() { return decorado.getPrecio().add(RECARGO); }
}

public class AdaptacionSinGluten extends ExtraPlato {

    private static final BigDecimal RECARGO = new BigDecimal("2.00");

    public AdaptacionSinGluten(Producto decorado) { super(decorado); }

    @Override
    public String getDescripcion() { return decorado.getDescripcion() + " (sin gluten)"; }

    @Override
    public BigDecimal getPrecio() { return decorado.getPrecio().add(RECARGO); }
}

public class ExtraRacionGrande extends ExtraPlato {

    private static final BigDecimal FACTOR = new BigDecimal("1.30");

    public ExtraRacionGrande(Producto decorado) { super(decorado); }

    @Override
    public String getDescripcion() { return decorado.getDescripcion() + ", ración grande"; }

    @Override
    public BigDecimal getPrecio() {
        return decorado.getPrecio().multiply(FACTOR).setScale(2, RoundingMode.HALF_UP);
    }
}

El cliente compone en ejecución, capa a capa, según lo que marque el usuario en la app:

Producto pedido = new Plato("Margarita", new BigDecimal("8.50"));
pedido = new ExtraDobleQueso(pedido);
pedido = new AdaptacionSinGluten(pedido);
pedido = new ExtraRacionGrande(pedido);

System.out.println(pedido.getDescripcion());
// Margarita con doble queso (sin gluten), ración grande
System.out.println(pedido.getPrecio());
// (8.50 + 1.50 + 2.00) * 1.30 = 15.60

Cada llamada a getPrecio() atraviesa la cebolla de fuera adentro y el resultado se construye de dentro afuera:

sequenceDiagram
    participant App
    participant Grande as :ExtraRacionGrande
    participant SinGluten as :AdaptacionSinGluten
    participant Queso as :ExtraDobleQueso
    participant Marg as :Plato

    App->>Grande: getPrecio()
    Grande->>SinGluten: getPrecio()
    SinGluten->>Queso: getPrecio()
    Queso->>Marg: getPrecio()
    Marg-->>Queso: 8.50
    Queso-->>SinGluten: 10.00
    SinGluten-->>Grande: 12.00
    Grande-->>App: 15.60

La cuenta final del patrón: 3 clases cubren las 8 combinaciones (y las 16 cuando llegue el bacon: una clase más). El extra nuevo no toca nada existente (OCP), cada extra vive en su clase (SRP), y como todo es un Producto, la LineaPedido del módulo 2 lo guarda sin enterarse de cuántas capas lleva. Nótese también que el orden importa y el modelo lo expresa: ración grande aplicada la última multiplica los extras; aplicada la primera, solo el precio base. Eso, que con banderas booleanas era imposible de expresar, aquí es simplemente el orden de envoltura.

Decoradores técnicos: reintentos y logging sobre Notificador

El mismo patrón brilla lejos de la carta. Pendiente desde el módulo 2: los envíos de notificaciones fallan a veces (redes, proveedores caídos) y queremos reintentos y trazas en RegistroEventos... sin tocar NotificadorPush, NotificadorSms ni NotificadorEmail, y sin duplicar esa lógica en los tres. Decoradores sobre la interfaz Notificador:

public class NotificadorConReintentos implements Notificador {

    private final Notificador decorado;
    private final int maxIntentos;

    public NotificadorConReintentos(Notificador decorado, int maxIntentos) {
        this.decorado = decorado;
        this.maxIntentos = maxIntentos;
    }

    @Override
    public void enviar(String destinatario, String mensaje) {
        NotificacionException ultima = null;
        for (int intento = 1; intento <= maxIntentos; intento++) {
            try {
                decorado.enviar(destinatario, mensaje);
                return;                                   // éxito: salimos
            } catch (NotificacionException e) {
                ultima = e;                               // fallo: siguiente intento
            }
        }
        throw new NotificacionException("Agotados " + maxIntentos + " intentos", ultima);
    }
}

public class NotificadorConLogging implements Notificador {

    private final Notificador decorado;

    public NotificadorConLogging(Notificador decorado) { this.decorado = decorado; }

    @Override
    public void enviar(String destinatario, String mensaje) {
        RegistroEventos.INSTANCE.info("Enviando a " + destinatario);
        try {
            decorado.enviar(destinatario, mensaje);
            RegistroEventos.INSTANCE.info("Enviado OK a " + destinatario);
        } catch (NotificacionException e) {
            RegistroEventos.INSTANCE.error("Fallo enviando a " + destinatario + ": " + e.getMessage());
            throw e;
        }
    }
}

Composición típica (y observa que el orden vuelve a significar cosas: logging por fuera de los reintentos traza el resultado final; por dentro, cada intento):

Notificador notificador =
        new NotificadorConLogging(
            new NotificadorConReintentos(
                new NotificadorSms(), 3));

Y la conexión con los creacionales es directa: el RegistroNotificadores de Suppliers puede registrar el canal ya decorado (() -> new NotificadorConLogging(new NotificadorConReintentos(new NotificadorSms(), 3))), de modo que ningún cliente sepa jamás cuántas capas lleva su notificador. Esta familia de decoradores técnicos (reintentos, logging, métricas, caché, cifrado) es de lo más rentable del patrón en sistemas reales — con un pariente cercano, Proxy, del que lo distinguiremos en su lección: misma mecánica, otra intención.

Decorator en el JDK: los streams de java.io

El ejemplo canónico del patrón lleva en Java desde 1996. Toda la E/S está construida como decoradores sobre InputStream/OutputStream:

InputStream entrada =
        new BufferedInputStream(          // decorador: añade buffering
            new GZIPInputStream(          // decorador: añade descompresión
                new FileInputStream("pedidos-2026.csv.gz")));  // componente concreto

BufferedInputStream y GZIPInputStream implementan InputStream y envuelven un InputStream (vía la base FilterInputStream, que es literalmente el rol Decorator): la doble relación exacta de nuestro ExtraPlato. Las combinaciones (fichero comprimido con buffer, socket cifrado sin buffer...) serían inviables por herencia; por decoración son una línea. En la misma familia: Collections.unmodifiableList(...) y Collections.synchronizedList(...) decoran una List añadiendo inmutabilidad o sincronización. Cuando encadenes constructores que reciben "lo mismo que devuelven", estás decorando.

Cuándo usarlo y cuándo no

Úsalo cuando:

  • Hay responsabilidades opcionales y combinables sobre un objeto (extras de platos; buffering/compresión/cifrado sobre streams) y las combinaciones se deciden en ejecución.
  • Quieres añadir comportamiento transversal (reintentos, trazas, métricas) a implementaciones existentes sin modificarlas ni duplicarlo en cada una.
  • La herencia es impracticable: explosión combinatoria, o la clase es final, o quieres decorar instancias concretas y no todas.

No lo uses cuando:

  • Solo hay una variación y no se combina con nada: una subclase o un parámetro es más simple (KISS). El patrón cobra sentido a partir de la combinatoria.
  • Necesitas quitar o consultar capas con frecuencia: la cebolla es opaca (no hay forma limpia de preguntar "¿lleva doble queso?" desde fuera) y desenvolver no está en el contrato. Si el pedido necesita listar sus extras para cocina, quizá el modelo correcto sea datos (una lista de extras con reglas de precio) y no decoradores — modelar con objetos no siempre gana a modelar con datos.
  • La interfaz Component es enorme: cada decorador debe implementarla entera, y veinte métodos de delegación para alterar uno es mala señal (revisa ISP antes que Decorator).
  • La identidad importa: el objeto decorado tiene otra identidad (==, equals) que el original, lo que rompe cachés y comparaciones ingenuas.

Relación con otros patrones (solo mención): Composite comparte interfaz y filosofía — decoradores y composites conviven de maravilla: puedes decorar una hoja del árbol; Proxy tiene idéntica estructura con intención de control de acceso en vez de añadir responsabilidades (careo en su lección y en la comparativa); Adapter también envuelve pero cambiando la interfaz; Strategy cambia las tripas del objeto mientras Decorator le cambia la piel; y las pilas de decoradores se montan cómodamente con las fábricas del módulo 2.

Errores Comunes y Consejos

  • Romper la delegación. Un decorador que olvida llamar a decorado.metodo() en algún método corta la cadena: las capas interiores dejan de ejecutarse silenciosamente. La base abstracta que delega por defecto (ExtraPlato) existe justo para esto.
  • Decoradores con conocimiento mutuo. Si ExtraRacionGrande hace instanceof ExtraDobleQueso para ajustar su cálculo, la independencia de capas ha muerto y el orden de envoltura se vuelve un campo de minas. Cada decorador solo conoce la interfaz.
  • Estado compartido con el decorado. Un decorador que cachea getPrecio() del envuelto se desincroniza si el plato cambia. Decoradores sin estado (o con estado propio inmutable) duermen mejor.
  • equals/hashCode heredados alegremente. ¿Una Margarita con queso equals una Margarita? Decide la semántica explícitamente; por defecto, identidades distintas, y documentado.
  • La cebolla infinita en el depurador. Diez capas anidadas hacen penoso el debugging (diez frames para un getPrecio()). Es un coste real del patrón: mantén las pilas cortas y con nombres claros; si el toString() de cada capa se autodescribe, el depurador se vuelve legible.
  • Consejo: cuando la composición de capas se repita (todo notificador de producción lleva logging + reintentos), no la esparzas por el código: dale nombre en un solo sitio — una fábrica, o el propio registro de Suppliers. Componer es barato; componer igual en todas partes a mano es una duplicación disfrazada.

Ejercicios

Ejercicio 1: el extra de bacon y el descuento de la casa

Escribe (a) ExtraBacon (+1,20 €, descripción "... con bacon"); (b) DescuentoDeLaCasa, un decorador que aplica un porcentaje de descuento al precio acumulado y añade " [descuento X% aplicado]" a la descripción. ¿Importa en qué posición de la cebolla se aplique el descuento? ¿Dónde lo colocarías y por qué?

Ejercicio 2: leer la cebolla

Dado este código, escribe la descripción y el precio resultantes, justificando el orden de los cálculos:

Producto p = new ExtraRacionGrande(
                 new ExtraDobleQueso(
                     new Plato("Prosciutto", new BigDecimal("9.90"))));

Ejercicio 3: ¿decorator o no?

Para cada necesidad, di si Decorator es adecuado y, si no, qué alternativa simple usarías: (1) que todas las notificaciones de PideYa, siempre, incluyan el prefijo "[PideYa]"; (2) medir la duración de las llamadas a PasarelaPago solo en el entorno de pruebas de rendimiento; (3) que cocina pueda listar los extras de un plato para prepararlo.

Soluciones

Solución 1:

public class ExtraBacon extends ExtraPlato {
    private static final BigDecimal RECARGO = new BigDecimal("1.20");
    public ExtraBacon(Producto decorado) { super(decorado); }
    @Override public String getDescripcion() { return decorado.getDescripcion() + " con bacon"; }
    @Override public BigDecimal getPrecio() { return decorado.getPrecio().add(RECARGO); }
}

public class DescuentoDeLaCasa extends ExtraPlato {
    private final BigDecimal porcentaje;           // p. ej. 10 = 10%
    public DescuentoDeLaCasa(Producto decorado, BigDecimal porcentaje) {
        super(decorado);
        this.porcentaje = porcentaje;
    }
    @Override public String getDescripcion() {
        return decorado.getDescripcion() + " [descuento " + porcentaje + "% aplicado]";
    }
    @Override public BigDecimal getPrecio() {
        BigDecimal factor = BigDecimal.ONE.subtract(
                porcentaje.movePointLeft(2));
        return decorado.getPrecio().multiply(factor).setScale(2, RoundingMode.HALF_UP);
    }
}

Sí importa: el descuento solo descuenta lo que tiene dentro. Aplicado en medio de la cebolla, los extras exteriores quedarían sin descontar. Debe ser la capa más externa para descontar sobre el total — y como esa regla es de negocio, mejor blindarla donde se compone (la fábrica/servicio que arma el producto), no confiarla a la memoria de cada programador.

Solución 2: de dentro afuera: base 9,90 → ExtraDobleQueso: 9,90 + 1,50 = 11,40 → ExtraRacionGrande: 11,40 × 1,30 = 14,82 €. Descripción, también de dentro afuera: "Prosciutto" → "Prosciutto con doble queso" → "Prosciutto con doble queso, ración grande". La ración grande, al ser la capa externa, multiplica también el recargo del queso — coherente con la política de la carta (una ración grande lleva más queso).

Solución 3: (1) No: si es siempre y para todos, no hay combinatoria ni opcionalidad; el prefijo pertenece al código común (la base o el punto único de envío). Decorator para lo invariable es ceremonia. (2) Sí: responsabilidad transversal, opcional (solo un entorno), sin tocar las pasarelas: un PasarelaConMetricas implements PasarelaPago que se compone únicamente en ese entorno — de hecho está en la frontera con Proxy, como veremos. (3) No con decoradores puros: la cebolla es opaca y desenvolverla con instanceof es luchar contra el patrón. Cocina necesita datos estructurados (lista de extras), señal de que ese caso pide modelar los extras como datos además de (o en vez de) como capas.

Conclusión

Decorator sustituye la pregunta "¿qué subclase necesito para esta combinación?" por "¿qué capas envuelvo hoy?": una interfaz común, un objeto base y decoradores minúsculos que añaden su responsabilidad y delegan el resto. En PideYa quedaron los extras componibles sobre Producto (ExtraDobleQueso, AdaptacionSinGluten, ExtraRacionGrande) y los decoradores técnicos sobre Notificador (NotificadorConReintentos, NotificadorConLogging), y de paso ya sabes leer la E/S de Java como lo que siempre fue: una cebolla de manual.

Hemos aprendido a traducir interfaces, tender puentes, armar árboles y envolver capas. Pero mientras tanto, en el checkout de PideYa, la app móvil sigue haciendo malabares: validar el carrito, pedir los impuestos a la familia del mercado, cobrar con la pasarela, construir el pedido, notificar... cinco subsistemas coordinados a mano desde cada cliente. A veces el mejor servicio que puede prestar un diseño no es una estructura ingeniosa, sino una puerta simple delante de la complejidad. Nos vemos en Facade.

Curso de Patrones de Diseño de Software

Módulo 1: Introducción a los Patrones de Diseño

Módulo 2: Patrones Creacionales

Módulo 3: Patrones Estructurales

Módulo 4: Patrones de Comportamiento

Módulo 5: Aplicación de Patrones de Diseño

Módulo 6: Patrones de Diseño Avanzados

Módulo 7: Recursos Adicionales y Conclusión

© Copyright 2026. Todos los derechos reservados