← Todos os padrões

Decorator

Structural · ver código-fonte no GitHub

Leia em: English · Português · Español

Category: Structural

O problema

Um objeto precisa de responsabilidades extras adicionadas a ele, mas nem toda instância precisa da mesma combinação de extras, e herança não consegue expressar isso com limpeza. Modelar cada combinação como uma subclasse (EspressoWithMilk, EspressoWithMilkAndSugar, EspressoWithSugarAndSugar, ...) explode combinatoriamente, e fica fixo em tempo de compilação — uma subclasse não pode ser adicionada ou removida de um objeto depois de construído. O que se precisa é de uma forma de envolver um objeto em camadas de comportamento, escolhidas e empilhadas em tempo de execução.

A solução

Dar ao wrapper a mesma interface da coisa que ele envolve, pra que possa substituí-la em qualquer lugar, e fazê-lo delegar ao objeto envolvido além de adicionar seu próprio comportamento antes ou depois. Empilhe wrappers pra combinar responsabilidades; cada um só conhece a interface, nunca a classe concreta por baixo.

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

Exemplo clássico

classic/Beverage é o exemplo canônico da cafeteria: um Espresso envolto em Milk e/ou Sugar, cada um adicionando seu próprio texto a description() e seus próprios centavos a costCents() em cima do que envolve. new Sugar(new Milk(new Espresso())) continua sendo um Beverage — nada distingue uma bebida decorada de uma simples no nível do tipo, que é exatamente o objetivo. BeverageDecoratorTest cobre uma bebida sem decoração, uma pilha de dois condimentos diferentes, e o mesmo condimento aplicado duas vezes (provando que decoradores compõem, não só alternam uma flag).

Exemplo aplicado: pipeline de enriquecimento de transação

applied/CoreTransactionProcessor é envolvido por FraudCheckDecorator, LgpdAuditDecorator (a lei brasileira de proteção de dados) e RateLimitDecorator — cada um uma preocupação que um pipeline de pagamentos real precisa, e cada um adicionável ou removível sem tocar no processador central nem nos outros. RateLimitDecorator também mostra que um decorador não precisa só adicionar comportamento depois de delegar: uma vez que um pagador ultrapassa a cota, ele retorna seu próprio resultado e nunca chama o resto da cadeia — o mesmo short-circuit que um limitador de taxa real precisa. TransactionProcessorDecoratorTest cobre a pilha completa aprovando uma transação normal (verificando que a trilha de auditoria está na ordem exata de envolvimento), a checagem de fraude sinalizando uma grande, e o limitador de taxa tanto deixando transações passarem quanto fazendo o short-circuit ao ultrapassar a cota.

Quando não usar

Cobertura de testes

100% de cobertura de instrução, 100% de cobertura de branch (JaCoCo). Reproduza você mesmo:

./gradlew :structural:decorator:jacocoTestReport

Relatório em structural/decorator/build/reports/jacoco/test/html/index.html.

Leitura complementar

Testes unitários

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 relatório completo de cobertura JaCoCo →