← Todos los patrones

State

Behavioral · ver código fuente en GitHub

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

Category: Behavioral

El problema

El comportamiento de un objeto necesita cambiar dependiendo de alguna condición interna, y qué transiciones son siquiera legales también depende de la condición actual. Modelar esto con un campo de estado más declaraciones if/switch esparcidas por cada método funciona hasta que crece el número de estados o transiciones — en ese punto cada método necesita conocer cada estado, las transiciones ilegales son fáciles de permitir por accidente, y agregar un estado nuevo significa tocar cada método existente que hace switch sobre él.

La solución

Darle a cada estado su propia clase implementando una interfaz compartida, y dejar que cada estado decida por sí mismo qué transiciones son legales desde ahí — normalmente devolviendo el objeto del siguiente estado, o rechazando la solicitud directamente. El objeto de contexto mantiene una referencia a su estado actual y le delega; nunca contiene un condicional de comprobación de estado por sí mismo.

classDiagram
    class Context {
        -state
        +request()
    }
    class State {
        <<interface>>
        +handle() State
    }
    class ConcreteStateA
    class ConcreteStateB
    Context o-- State
    State <|.. ConcreteStateA
    State <|.. ConcreteStateB
    ConcreteStateA --> ConcreteStateB : transitions to

Ejemplo clásico

classic/TrafficLight mantiene un TrafficLightState y le delega advance(); RedState, GreenState, y YellowState solo saben cada uno una cosa: qué estado viene después. TrafficLight en sí no tiene ningún if (color == "RED") en ningún lado. TrafficLightTest recorre un ciclo completo rojo→verde→amarillo→rojo.

Ejemplo aplicado: ciclo de vida de una transacción

applied/TransactionState rechaza toda transición por defecto; PendingState sobrescribe solo startProcessing(), ProcessingState sobrescribe solo settle() y fail(), y SettledState/FailedState no sobrescriben nada — son terminales, así que cada intento de transición falla correctamente. Este es el mismo ciclo de vida PENDING → PROCESSING → SETTLED/FAILED sobre el que notifica el módulo Observer de este repositorio — la diferencia es para qué sirve cada patrón: Observer distribuye un cambio de estado a los oyentes interesados una vez que ya ocurrió; State es lo que realmente decide si ese cambio es legal en primer lugar. Un gateway de pagos real necesita ambos, normalmente en capas: State aplica la transición, luego algo publica el evento al que reaccionan los oyentes de Observer. TransactionTest cubre ambos caminos terminales (settled, failed) y tres casos de transición ilegal, incluyendo que ninguno de los dos estados terminales permite ninguna transición adicional.

Cuándo no usarlo

Cobertura de pruebas

100% de cobertura de instrucciones (JaCoCo; la cobertura de ramas reporta n/a — nada aquí ramifica, cada método de cada estado es incondicional). Reprodúzcalo usted mismo:

./gradlew :behavioral:state:jacocoTestReport

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

Lecturas adicionales

Pruebas unitarias

src/test/java/com/designpatterns/behavioral/state/classic/TrafficLightTest.java
package com.designpatterns.behavioral.state.classic;

import org.junit.jupiter.api.Test;

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

class TrafficLightTest {

    @Test
    void cyclesThroughRedGreenYellowAndBackToRed() {
        TrafficLight light = new TrafficLight();

        assertThat(light.currentColor()).isEqualTo("RED");

        light.advance();
        assertThat(light.currentColor()).isEqualTo("GREEN");

        light.advance();
        assertThat(light.currentColor()).isEqualTo("YELLOW");

        light.advance();
        assertThat(light.currentColor()).isEqualTo("RED");
    }
}
src/test/java/com/designpatterns/behavioral/state/applied/TransactionTest.java
package com.designpatterns.behavioral.state.applied;

import org.junit.jupiter.api.Test;

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

class TransactionTest {

    @Test
    void startsPendingAndFollowsTheHappyPathToSettled() {
        Transaction transaction = new Transaction("tx-1");
        assertThat(transaction.id()).isEqualTo("tx-1");
        assertThat(transaction.status()).isEqualTo("PENDING");

        transaction.startProcessing();
        assertThat(transaction.status()).isEqualTo("PROCESSING");

        transaction.settle();
        assertThat(transaction.status()).isEqualTo("SETTLED");
    }

    @Test
    void followsTheAlternatePathToFailed() {
        Transaction transaction = new Transaction("tx-2");

        transaction.startProcessing();
        transaction.fail();

        assertThat(transaction.status()).isEqualTo("FAILED");
    }

    @Test
    void rejectsSettlingBeforeProcessingStarted() {
        Transaction transaction = new Transaction("tx-3");

        assertThatThrownBy(transaction::settle).isInstanceOf(IllegalStateException.class);
        assertThat(transaction.status()).isEqualTo("PENDING");
    }

    @Test
    void rejectsAnyTransitionOnceSettledIsTerminal() {
        Transaction transaction = new Transaction("tx-4");
        transaction.startProcessing();
        transaction.settle();

        assertThatThrownBy(transaction::startProcessing).isInstanceOf(IllegalStateException.class);
        assertThatThrownBy(transaction::settle).isInstanceOf(IllegalStateException.class);
        assertThatThrownBy(transaction::fail).isInstanceOf(IllegalStateException.class);
    }

    @Test
    void rejectsAnyTransitionOnceFailedIsTerminal() {
        Transaction transaction = new Transaction("tx-5");
        transaction.startProcessing();
        transaction.fail();

        assertThatThrownBy(transaction::startProcessing).isInstanceOf(IllegalStateException.class);
    }
}

Ver informe completo de cobertura JaCoCo →