← Todos os padrões

State

Behavioral · ver código-fonte no GitHub

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

Category: Behavioral

O problema

O comportamento de um objeto precisa mudar dependendo de alguma condição interna, e quais transições são sequer legais também depende da condição atual. Modelar isso com um campo de status mais declarações if/switch espalhadas por todo método funciona até o número de estados ou transições crescer — nesse ponto todo método precisa conhecer todo estado, transições ilegais são fáceis de permitir por acidente, e adicionar um estado novo significa tocar em todo método existente que faz switch sobre ele.

A solução

Dar a cada estado sua própria classe implementando uma interface compartilhada, e deixar cada estado decidir por conta própria quais transições são legais a partir dali — normalmente retornando o objeto do próximo estado, ou rejeitando a solicitação de vez. O objeto de contexto guarda uma referência pro seu estado atual e delega a ele; ele nunca contém um condicional de checagem de estado por conta própria.

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

Exemplo clássico

classic/TrafficLight guarda um TrafficLightState e delega advance() a ele; RedState, GreenState, e YellowState sabem cada um só uma coisa: qual estado vem a seguir. TrafficLight em si não tem nenhum if (color == "RED") em lugar nenhum. TrafficLightTest percorre um ciclo completo vermelho→verde→amarelo→vermelho.

Exemplo aplicado: ciclo de vida de transação

applied/TransactionState rejeita toda transição por padrão; PendingState sobrescreve só startProcessing(), ProcessingState sobrescreve só settle() e fail(), e SettledState/FailedState não sobrescrevem nada — são terminais, então toda tentativa de transição corretamente falha. Esse é o mesmo ciclo de vida PENDING → PROCESSING → SETTLED/FAILED sobre o qual o módulo Observer deste repositório notifica — a diferença é pra que cada padrão serve: Observer distribui uma mudança de status pra ouvintes interessados depois que ela já aconteceu; State é o que de fato decide se essa mudança é legal em primeiro lugar. Um gateway de pagamento real precisa dos dois, normalmente em camadas: State aplica a transição, depois algo publica o evento ao qual os ouvintes do Observer reagem. TransactionTest cobre os dois caminhos terminais (settled, failed) e três casos de transição ilegal, incluindo que nenhum dos dois estados terminais permite qualquer transição adicional.

Quando não usar

Cobertura de testes

100% de cobertura de instrução (JaCoCo; cobertura de branch reporta n/a — nada aqui ramifica, todo método de todo estado é incondicional). Reproduza você mesmo:

./gradlew :behavioral:state:jacocoTestReport

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

Leitura complementar

Testes unitários

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