← Todos os padrões

Chain of Responsibility

Behavioral · ver código-fonte no GitHub

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

Category: Behavioral

O problema

Uma solicitação pode precisar ser tratada por um entre vários handlers possíveis, mas quem envia não deveria precisar saber qual, nem ter a lógica de decisão de escolha embutida. Uma única cadeia de if/else if checando a condição de elegibilidade de cada handler funciona no início, mas coloca a regra de negócio de cada handler num único lugar, acoplada à regra de todo outro handler, e adicionar um handler novo significa editar esse método compartilhado.

A solução

Encadear os handlers, cada um segurando uma referência pro próximo. Cada handler decide por conta própria se consegue (ou deve) tratar a solicitação; se não, passa ela adiante. Quem envia só conversa com o primeiro elo — não sabe quão longa é a cadeia, nem qual elo de fato processa a solicitação.

classDiagram
    class Handler {
        -next
        +handle(request)
    }
    class ConcreteHandlerA
    class ConcreteHandlerB
    class ConcreteHandlerC
    Handler <|-- ConcreteHandlerA
    Handler <|-- ConcreteHandlerB
    Handler <|-- ConcreteHandlerC
    ConcreteHandlerA --> ConcreteHandlerB : next
    ConcreteHandlerB --> ConcreteHandlerC : next

Exemplo clássico

classic/Approver é a cadeia canônica de aprovação de compra: SupervisorManagerDirector, cada um com seu próprio teto de aprovação. Uma solicitação dentro do limite do Supervisor nunca chega ao Manager; uma solicitação além do limite de todo mundo cai fora do fim da cadeia com um resultado claro de "nenhum aprovador disponível", em vez de uma exceção ou um no-op silencioso. ApproverTest cobre um valor parando em cada um dos três níveis, mais o caso além de todo mundo.

Exemplo aplicado: pipeline de compliance de transação

applied/ComplianceHandler encadeia KycHandlerAmlHandlerLimitHandlerFraudHandler — verificação de identidade antes de triagem contra watchlist antes da checagem de limite de negócio antes da heurística de fraude (mais cara), espelhando como um pipeline de compliance real de fato é ordenado: as checagens mais baratas e mais decisivas primeiro. O primeiro handler a rejeitar uma transação para a cadeia imediatamente; handlers seguintes nem chegam a vê-la, que é exatamente o que impede, digamos, a heurística de fraude de rodar numa transação que já nem ia passar no KYC. ComplianceHandlerTest cobre uma transação passando por toda checagem, e cada handler individual sendo o que rejeita.

Quando não usar

Cobertura de testes

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

./gradlew :behavioral:chainofresponsibility:jacocoTestReport

Relatório em behavioral/chainofresponsibility/build/reports/jacoco/test/html/index.html.

Leitura complementar

Testes unitários

src/test/java/com/designpatterns/behavioral/chainofresponsibility/classic/ApproverTest.java
package com.designpatterns.behavioral.chainofresponsibility.classic;

import org.junit.jupiter.api.Test;

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

class ApproverTest {

    private final Approver chain = new Supervisor().next(new Manager().next(new Director()));

    @Test
    void aSmallAmountStopsAtTheSupervisor() {
        assertThat(chain.approve(500_00L)).isEqualTo("Supervisor approved 50000 cents");
    }

    @Test
    void aMidSizedAmountEscalatesPastTheSupervisorToTheManager() {
        assertThat(chain.approve(5_000_00L)).isEqualTo("Manager approved 500000 cents");
    }

    @Test
    void aLargeAmountEscalatesAllTheWayToTheDirector() {
        assertThat(chain.approve(50_000_00L)).isEqualTo("Director approved 5000000 cents");
    }

    @Test
    void anAmountBeyondEveryLinksLimitFallsOffTheEndOfTheChain() {
        assertThat(chain.approve(1_000_000_00L)).isEqualTo("No approver available for 100000000 cents");
    }
}
src/test/java/com/designpatterns/behavioral/chainofresponsibility/applied/ComplianceHandlerTest.java
package com.designpatterns.behavioral.chainofresponsibility.applied;

import org.junit.jupiter.api.Test;

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

class ComplianceHandlerTest {

    private static final long LIMIT_CENTS = 50_000_00L;

    private final ComplianceHandler chain =
            new KycHandler().next(new AmlHandler().next(new LimitHandler(LIMIT_CENTS).next(new FraudHandler())));

    @Test
    void approvesATransactionThatClearsEveryCheck() {
        ComplianceTransaction transaction = new ComplianceTransaction("payer-1", 10_000_00L, true, false, false);

        ComplianceResult result = chain.check(transaction);

        assertThat(result.approved()).isTrue();
        assertThat(result.reason()).isNull();
    }

    @Test
    void anUnverifiedPayerIsRejectedByKycBeforeAnyLaterCheckRuns() {
        ComplianceTransaction transaction = new ComplianceTransaction("payer-2", 10_000_00L, false, true, true);

        ComplianceResult result = chain.check(transaction);

        assertThat(result.approved()).isFalse();
        assertThat(result.reason()).isEqualTo("KYC: payer not verified");
    }

    @Test
    void aWatchlistedPayerIsRejectedByAml() {
        ComplianceTransaction transaction = new ComplianceTransaction("payer-3", 10_000_00L, true, true, false);

        ComplianceResult result = chain.check(transaction);

        assertThat(result.reason()).isEqualTo("AML: payer is on a watchlist");
    }

    @Test
    void anAmountAboveTheThresholdIsRejectedByTheLimitHandler() {
        ComplianceTransaction transaction = new ComplianceTransaction("payer-4", 60_000_00L, true, false, false);

        ComplianceResult result = chain.check(transaction);

        assertThat(result.reason()).isEqualTo("LIMIT: amount exceeds the 5000000 cent threshold");
    }

    @Test
    void aHighRiskFlaggedTransactionThatClearsEverythingElseIsRejectedByFraud() {
        ComplianceTransaction transaction = new ComplianceTransaction("payer-5", 10_000_00L, true, false, true);

        ComplianceResult result = chain.check(transaction);

        assertThat(result.reason()).isEqualTo("FRAUD: transaction flagged as high risk");
    }
}

Ver relatório completo de cobertura JaCoCo →