← Todos los patrones

Chain of Responsibility

Behavioral · ver código fuente en GitHub

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

Category: Behavioral

El problema

Una solicitud puede necesitar ser manejada por uno entre varios handlers posibles, pero quien la envía no debería tener que saber cuál, ni tener la lógica de decisión de selección incrustada. Una única cadena de if/else if comprobando la condición de elegibilidad de cada handler funciona al principio, pero pone la regla de negocio de cada handler en un solo lugar, acoplada a la regla de cualquier otro handler, y agregar un handler nuevo significa editar ese método compartido.

La solución

Encadenar los handlers, cada uno sosteniendo una referencia al siguiente. Cada handler decide por sí mismo si puede (o debe) manejar la solicitud; si no, la pasa adelante. Quien envía solo habla con el primer eslabón — no sabe cuán larga es la cadena, ni qué eslabón procesa realmente la solicitud.

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

Ejemplo clásico

classic/Approver es la cadena canónica de aprobación de compra: SupervisorManagerDirector, cada uno con su propio tope de aprobación. Una solicitud dentro del límite del Supervisor nunca llega al Manager; una solicitud más allá del límite de todos cae fuera del final de la cadena con un resultado claro de "ningún aprobador disponible", en vez de una excepción o un no-op silencioso. ApproverTest cubre un monto deteniéndose en cada uno de los tres niveles, más el caso más allá de todos.

Ejemplo aplicado: pipeline de cumplimiento de transacciones

applied/ComplianceHandler encadena KycHandlerAmlHandlerLimitHandlerFraudHandler — verificación de identidad antes de la detección contra listas de vigilancia, antes de la verificación del límite de negocio, antes de la heurística de fraude (más costosa), reflejando cómo se ordena realmente un pipeline de cumplimiento real: las verificaciones más baratas y más decisivas primero. El primer handler en rechazar una transacción detiene la cadena de inmediato; los handlers posteriores ni siquiera llegan a verla, que es exactamente lo que evita que, digamos, la heurística de fraude corra sobre una transacción que de todas formas nunca iba a pasar el KYC. ComplianceHandlerTest cubre una transacción que pasa cada verificación, y cada handler individual siendo el que rechaza.

Cuándo no usarlo

Cobertura de pruebas

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

./gradlew :behavioral:chainofresponsibility:jacocoTestReport

Informe en behavioral/chainofresponsibility/build/reports/jacoco/test/html/index.html.

Lecturas adicionales

Pruebas unitarias

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 informe completo de cobertura JaCoCo →