← Todos los patrones

Decorator

Structural · ver código fuente en GitHub

Leer en: English · Português · Español

Category: Structural

El problema

Un objeto necesita responsabilidades extra agregadas, pero no toda instancia necesita la misma combinación de extras, y la herencia no puede expresar eso con limpieza. Modelar cada combinación como una subclase (EspressoWithMilk, EspressoWithMilkAndSugar, EspressoWithSugarAndSugar, ...) explota combinatoriamente, y queda fijo en tiempo de compilación — una subclase no se puede agregar ni quitar de un objeto una vez construido. Lo que se necesita es una forma de envolver un objeto en capas de comportamiento, elegidas y apiladas en tiempo de ejecución.

La solución

Darle al envoltorio la misma interfaz de lo que envuelve, para que pueda sustituirlo en cualquier lugar, y hacer que delegue al objeto envuelto además de agregar su propio comportamiento antes o después. Apile envoltorios para combinar responsabilidades; cada uno solo conoce la interfaz, nunca la clase concreta debajo.

classDiagram
    class Component {
        <<interface>>
    }
    class ConcreteComponent
    class Decorator {
        -component
    }
    class ConcreteDecoratorA
    class ConcreteDecoratorB
    Component <|.. ConcreteComponent
    Component <|.. Decorator
    Decorator o-- Component
    Decorator <|-- ConcreteDecoratorA
    Decorator <|-- ConcreteDecoratorB

Ejemplo clásico

classic/Beverage es el ejemplo canónico de la cafetería: un Espresso envuelto en Milk y/o Sugar, cada uno agregando su propio texto a description() y sus propios centavos a costCents() encima de lo que envuelve. new Sugar(new Milk(new Espresso())) sigue siendo un Beverage — nada distingue una bebida decorada de una simple a nivel de tipo, que es exactamente el punto. BeverageDecoratorTest cubre una bebida sin decorar, una pila de dos condimentos distintos, y el mismo condimento aplicado dos veces (probando que los decoradores se componen, no solo alternan una bandera).

Ejemplo aplicado: pipeline de enriquecimiento de transacciones

applied/CoreTransactionProcessor está envuelto por FraudCheckDecorator, LgpdAuditDecorator (la ley brasileña de protección de datos) y RateLimitDecorator — cada uno una preocupación que un pipeline de pagos real necesita, y cada uno se puede agregar o quitar sin tocar el procesador central ni los demás. RateLimitDecorator también muestra que un decorador no tiene que solo agregar comportamiento después de delegar: una vez que un pagador supera la cuota, devuelve su propio resultado y nunca llama al resto de la cadena — el mismo corte que un limitador de tasa real necesita. TransactionProcessorDecoratorTest cubre la pila completa aprobando una transacción normal (verificando que el rastro de auditoría esté en el orden exacto de envoltura), la verificación de fraude marcando una grande, y el limitador de tasa tanto dejando pasar transacciones como cortando al superar la cuota.

Cuándo no usarlo

Cobertura de pruebas

100% de cobertura de instrucciones, 100% de cobertura de ramas (JaCoCo). Reprodúzcalo usted mismo:

./gradlew :structural:decorator:jacocoTestReport

Informe en structural/decorator/build/reports/jacoco/test/html/index.html.

Lecturas adicionales

Pruebas unitarias

src/test/java/com/designpatterns/structural/decorator/classic/BeverageDecoratorTest.java
package com.designpatterns.structural.decorator.classic;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class BeverageDecoratorTest {

    @Test
    void aPlainBeverageHasNoCondiments() {
        Beverage order = new Espresso();

        assertThat(order.description()).isEqualTo("Espresso");
        assertThat(order.costCents()).isEqualTo(250L);
    }

    @Test
    void stacksDescriptionAndCostForEachCondimentInWrappingOrder() {
        Beverage order = new Sugar(new Milk(new Espresso()));

        assertThat(order.description()).isEqualTo("Espresso + Milk + Sugar");
        assertThat(order.costCents()).isEqualTo(320L);
    }

    @Test
    void theSameCondimentCanBeAppliedMoreThanOnce() {
        Beverage order = new Sugar(new Sugar(new Espresso()));

        assertThat(order.description()).isEqualTo("Espresso + Sugar + Sugar");
        assertThat(order.costCents()).isEqualTo(290L);
    }
}
src/test/java/com/designpatterns/structural/decorator/applied/TransactionProcessorDecoratorTest.java
package com.designpatterns.structural.decorator.applied;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class TransactionProcessorDecoratorTest {

    @Test
    void approvesANormalTransactionAndRecordsEachLayersNoteInWrappingOrder() {
        TransactionProcessor pipeline = new LgpdAuditDecorator(new FraudCheckDecorator(new CoreTransactionProcessor()));
        Transaction transaction = new Transaction("tx-1", 10_000_00L, "payer-1");

        ProcessingResult result = pipeline.process(transaction);

        assertThat(result.approved()).isTrue();
        assertThat(result.auditTrail()).containsExactly(
                "core: transaction accepted",
                "fraud-check: amount within normal range",
                "lgpd-audit: access to payer payer-1 logged for compliance"
        );
    }

    @Test
    void flagsATransactionAboveTheFraudThreshold() {
        TransactionProcessor pipeline = new FraudCheckDecorator(new CoreTransactionProcessor());
        Transaction transaction = new Transaction("tx-2", 60_000_00L, "payer-2");

        ProcessingResult result = pipeline.process(transaction);

        assertThat(result.approved()).isFalse();
        assertThat(result.auditTrail()).anyMatch(note -> note.contains("fraud-check"));
    }

    @Test
    void rateLimitDecoratorShortCircuitsWithoutCallingTheRestOfThePipelineOnceTheQuotaIsExceeded() {
        TransactionProcessor pipeline = new RateLimitDecorator(new CoreTransactionProcessor(), 2);
        Transaction transaction = new Transaction("tx-3", 1_00L, "payer-3");

        pipeline.process(transaction);
        pipeline.process(transaction);
        ProcessingResult thirdCall = pipeline.process(transaction);

        assertThat(thirdCall.approved()).isFalse();
        assertThat(thirdCall.auditTrail()).containsExactly("rate-limit: payer exceeded 2 requests");
    }

    @Test
    void rateLimitDecoratorPassesThroughAndAnnotatesCallsWithinQuota() {
        TransactionProcessor pipeline = new RateLimitDecorator(new CoreTransactionProcessor(), 5);
        Transaction transaction = new Transaction("tx-4", 1_00L, "payer-4");

        ProcessingResult result = pipeline.process(transaction);

        assertThat(result.approved()).isTrue();
        assertThat(result.auditTrail()).containsExactly(
                "core: transaction accepted",
                "rate-limit: within quota (1/5)"
        );
    }
}

Ver informe completo de cobertura JaCoCo →