Design Patterns: State (Parte 5)

·5 min de leitura

Contexto

Depois do Adapter (Parte 4), voltamos aos padrões comportamentais. State aparece quando um objeto precisa mudar o que faz dependendo do seu estado interno, e você quer evitar aquele método que vira um festival de if, switch e flags.

A intenção do State é permitir que um objeto altere seu comportamento quando o estado muda, quase como se ele "mudasse de classe". O ponto mais importante é o seguinte: estado não é só um enum com regras espalhadas em vários lugares; no State, o estado vira polimorfismo, e as transições ficam mais locais e legíveis.

Isso é muito parecido com Strategy no diagrama, e é justamente aí que muita gente se confunde. Strategy costuma variar um algoritmo por escolha externa (configuração, input, preferências). State varia comportamento porque o próprio fluxo evolui: o objeto transiciona de um estado para o outro e isso muda o que é permitido fazer em seguida.

Exemplo na prática

Um pedido simples: DraftSubmittedApproved. Cada estado sabe o que pode fazer.

A interface define o contrato que todo estado precisa cumprir. Qualquer estado do pedido vai precisar responder às ações submit e approve, mesmo que seja para recusar.

<?php

declare(strict_types=1);

interface OrderState
{
    public function submit(Order $order): void;

    public function approve(Order $order): void;
}

Order é o contexto: guarda o estado atual e expõe as ações de alto nível. Ele não decide nada sozinho, apenas delega para o estado. O método transitionTo é a porta de entrada para trocar de estado de dentro das próprias implementações.

final class Order
{
    public function __construct(private OrderState $state) {}

    public function state(): OrderState
    {
        return $this->state;
    }

    public function transitionTo(OrderState $state): void
    {
        $this->state = $state;
    }

    public function submit(): void
    {
        $this->state->submit($this);
    }

    public function approve(): void
    {
        $this->state->approve($this);
    }
}

DraftState é o estado inicial. Um rascunho pode ser enviado (transiciona para SubmittedState), mas não pode ser aprovado diretamente, o que gera uma exceção de domínio.

final class DraftState implements OrderState
{
    public function submit(Order $order): void
    {
        $order->transitionTo(new SubmittedState());
    }

    public function approve(Order $order): void
    {
        throw new DomainException('Pedido em rascunho não pode ser aprovado.');
    }
}

SubmittedState representa um pedido já enviado. Enviar de novo não faz sentido, mas podemos aprovar, levando o pedido para ApprovedState.

final class SubmittedState implements OrderState
{
    public function submit(Order $order): void
    {
        throw new DomainException('Pedido já foi enviado.');
    }

    public function approve(Order $order): void
    {
        $order->transitionTo(new ApprovedState());
    }
}

ApprovedState é o estado final. Qualquer ação aqui é inválida: o pedido já foi aprovado e não há mais transições possíveis.

final class ApprovedState implements OrderState
{
    public function submit(Order $order): void
    {
        throw new DomainException('Pedido já aprovado.');
    }

    public function approve(Order $order): void
    {
        throw new DomainException('Pedido já aprovado.');
    }
}

Orquestrando

Agora basta criar o pedido com o estado inicial e chamar as ações. O contexto delega tudo para o estado correto, e as transições inválidas estouram exceção:

$order = new Order(new DraftState());

$order->submit();  // Draft → Submitted
$order->approve(); // Submitted → Approved

$order->approve(); // DomainException: Pedido já aprovado.

Repare que quem usa Order não precisa saber qual estado está ativo. Você chama submit() ou approve() e o próprio estado decide se a ação faz sentido ou não. Se o fluxo for respeitado (Draft → Submitted → Approved), tudo funciona. Se alguém tentar pular etapas ou repetir uma ação, o estado atual rejeita com uma exceção clara.

Esse é o ganho real do padrão: o código cliente fica limpo, sem if para verificar "em que fase o pedido está", e qualquer regra nova de transição fica isolada dentro do estado correspondente.

O que esse código faz é empurrar as regras para o lugar mais próximo de onde elas são relevantes. Cada classe *State concentra as regras e transições daquele momento do workflow, e quando algo é inválido você ganha um erro de domínio explícito (em vez de silêncio ou combinações de flags inconsistentes).

Persistência e State

Em aplicações reais, você geralmente persiste um código de estado (draft, submitted…) e, ao carregar do banco, recria o estado do objeto via factory/mapper. A implementação muda conforme a stack, mas a ideia central do padrão continua valendo: o domínio se beneficia quando as regras de transição não ficam espalhadas.

Quando não usar

Se a feature tiver dois estados e uma transição óbvia (tipo ativo/inativo), um enum com um match pequeno resolve bem e criar classes para isso é overengineering.

O outro caso é quando o número de estados cresce demais. Imagine um Order que pode estar em 15 estados diferentes, com transições cruzadas entre quase todos eles. Se você está criando 15 classes e cada uma precisa implementar 10 métodos, o problema provavelmente não é falta de padrão, é que o conceito de "pedido" está carregando responsabilidades demais. A saída costuma ser quebrar o domínio em conceitos menores (pagamento, entrega, aprovação), cada um com seu próprio ciclo de vida simples, em vez de empilhar mais classes de estado num único objeto.

Conclusão

State organiza transições e evita "herança para variar comportamento" ou métodos gigantes. É um dos padrões mais úteis em domínios com workflow.

Referências

  • Design Patterns (GoF).
  • Refactoring Guru – State: https://refactoring.guru/design-patterns/state