ComportamentalIntermediário7 min de leitura9 seções1994, Marco de referência: a ideia é mais velha que a data

State

Transforma cada estado de um objeto em uma classe própria: o comportamento muda conforme o estado atual, e é o próprio estado que decide a próxima transição.

Em 10 segundos

Cada estado do objeto vira uma classe com o comportamento daquele estado; o contexto só delega. Quem decide a próxima transição é o próprio estado — não o cliente, não um switch central.

  • #maquina-de-estados
  • #transicoes
  • #polimorfismo
  • #gof
Analogia

O semáforo

O mesmo semáforo se comporta diferente em cada cor: no verde, carros passam; no amarelo, atenção; no vermelho, todo mundo para. Ninguém de fora escolhe a próxima cor — é o próprio estado que sabe seu sucessor: verde sabe que vira amarelo, amarelo sabe que vira vermelho. O objeto é um só (o semáforo na esquina), mas quem responde a cada instante é o estado atual, e a transição já vem embutida nele.

01Conceito

O problema

Um pedido novo cancela de graça; pago, cancela com estorno; enviado, não cancela. Com um campo status, cada método do pedido vira um switch — e a mesma escadinha de casos se repete em avancar(), cancelar(), estornar()...

A máquina de estados existe, mas está espalhada e implícita: adicionar o estado 'em separação' significa caçar todos os switches do sistema, e nada garante que uma transição proibida não aconteça por um caminho esquecido.

A raiz do problema é organizar o código pelo eixo errado. O switch agrupa por operação (todos os comportamentos de cancelar juntos), mas a coesão real está no estado: 'tudo que um pedido pago pode fazer' pertence junto. O State reorganiza o código por estado — cada classe é uma linha da tabela de transições.

A diferença para o Strategy é sutil na estrutura (ambos delegam a um objeto trocável) e enorme na intenção: no Strategy, o CLIENTE escolhe o algoritmo e as estratégias não se conhecem; no State, os estados conhecem uns aos outros e trocam a si mesmos — o cliente nem sabe qual está ativo. Strategy é escolha externa e estável; State é evolução interna e dinâmica.

Nem toda máquina pede o padrão: para máquinas grandes, planas e dirigidas por eventos, uma tabela declarativa de transições ou uma biblioteca de statecharts costuma expressar melhor (visualização, guards, estados aninhados). O State em classes brilha quando cada estado carrega comportamento rico, não só um nome.

Como se encaixa
  1. Cliente
  2. avancar()Pedido (contexto)
  3. delega ao estado atualEstado: Novo
  4. transicionaEstado: Pago
  5. transicionaEstado: Enviado
O cliente sempre chama o contexto; o estado atual responde e, ao concluir, aponta o contexto para o próximo estado — a transição é decisão interna.
02Anatomia

Navegue pelas camadas

Camada 1/3Contexto

Camada 1

Contexto

guarda o estado atual e delega

É o objeto que o mundo conhece (Pedido, Conexão, Player). Mantém a referência ao estado atual, delega cada operação a ele e oferece mudarPara() para os estados trocarem o ponteiro. Não contém nenhum if de status.

No código

avancar(): void { this.estado.avancar(this); }

Se violar: um if (status === ...) dentro do contexto é a máquina vazando de volta — em pouco tempo os switches se multiplicam de novo.

03Estrutura

Estrutura

Diagrama de classes
04Implementação

Código

interface EstadoPedido {
  avancar(pedido: Pedido): void;
  cancelar(pedido: Pedido): void;
}

class Pedido {
  private estado: EstadoPedido = new Novo();
  mudarPara(estado: EstadoPedido): void { this.estado = estado; }
  avancar(): void { this.estado.avancar(this); }
  cancelar(): void { this.estado.cancelar(this); }
}

class Novo implements EstadoPedido {
  avancar(p: Pedido): void {
    console.log("Pagamento confirmado");
    p.mudarPara(new Pago());        // o ESTADO decide a transicao
  }
  cancelar(p: Pedido): void {
    console.log("Cancelado sem custo");
  }
}

class Pago implements EstadoPedido {
  avancar(p: Pedido): void {
    console.log("Despachado para o cliente");
    p.mudarPara(new Enviado());
  }
  cancelar(p: Pedido): void {
    console.log("Cancelado com estorno");
  }
}

class Enviado implements EstadoPedido {
  avancar(): void { console.log("Ja enviado - nada a avancar"); }
  cancelar(): void { console.log("Nao e possivel cancelar apos envio"); }
}
05No mundo real

Casos de uso

Rastreamento de encomendas em logística

Cenário

Uma encomenda passa por postada → em trânsito → saiu para entrega → entregue, com desvios (extraviada, devolvida). Cada situação permite ações diferentes: redirecionar só antes de sair para entrega; abrir disputa só após extravio.

Como o padrão entra

Cada situação é um estado que implementa redirecionar(), reportarProblema(), confirmarEntrega(). O scan do leitor de código de barras chama avancar() e o estado atual decide o sucessor — inclusive o desvio para Extraviada após N dias sem scan.

Trade-off: Estados de exceção (extraviada, devolvida) multiplicam as combinações; sem disciplina, cada 'caso especial' novo tenta virar flag dentro de um estado existente em vez de estado próprio.

Ciclo de vida de uma cobrança Pix

Cenário

Uma cobrança nasce ativa, pode ser paga, expirar ou ser removida pelo recebedor. Pagar uma cobrança expirada deve falhar; remover uma paga, também — e cada erro tem mensagem e código próprios.

Como o padrão entra

Ativa, Paga, Expirada e Removida implementam pagar(), remover(), consultar(). O webhook de pagamento chama cobranca.pagar(): se o estado é Ativa, transiciona para Paga e dispara a liquidação; se Expirada, responde o erro certo sem nenhum if no serviço.

Trade-off: O estado precisa ser persistido e recarregado: mapear linha do banco → objeto de estado exige uma factory, e transições concorrentes (pagamento e expiração no mesmo instante) pedem lock otimista — o padrão não resolve concorrência sozinho.

Player de vídeo em streaming

Cenário

O botão play/pause do player faz coisas diferentes conforme o momento: tocando pausa, pausado retoma, buffering ignora o clique, erro tenta recarregar.

Como o padrão entra

Tocando, Pausado, Buffering e ComErro implementam aoClicarPlay(), aoTerminarBuffer(), aoFalhar(). O evento de rede que enche o buffer chega ao estado Buffering, que decide voltar para Tocando — a UI só repassa eventos ao contexto.

Trade-off: Eventos assíncronos podem chegar 'atrasados' para um estado que já mudou (o buffer terminou depois do usuário pausar) — cada estado precisa decidir conscientemente o que ignorar.

Passo a passoComo montar a máquina
  1. 1

    Listar estados e operações

    Quais situações o objeto vive e quais operações mudam de comportamento nelas. Isso vira a interface de estado.

  2. 2

    Uma classe por estado

    Cada estado implementa as operações — inclusive para recusar. Recusar também é comportamento explícito.

  3. 3

    Delegar no contexto

    O Pedido (ou Player) guarda o estado atual e só repassa. Nenhum `if (status === ...)` no contexto.

  4. 4

    Deixar o estado transicionar

    Quem decide o próximo é o estado atual (`p.mudarPara(new Pago())`), não o cliente. Assignment externo fura a máquina.

O State que o cliente troca na mão

Compila, roda, passa no code review. E quebra depois.

Há classes Novo/Pago/Enviado e o contexto delega — mas pedido.estado é público e o chamador atribui o próximo estado. A máquina existe no diagrama e é ignorada no código.

interface EstadoPedido {
  avancar(p: Pedido): void;
  cancelar(p: Pedido): void;
}

class Pedido {
  estado: EstadoPedido = new Novo();
  avancar() { this.estado.avancar(this); }
  cancelar() { this.estado.cancelar(this); }
}

// As classes de estado existem... e o cliente fura a maquina:
const pedido = new Pedido();
pedido.estado = new Enviado(); // pulou o pagamento

// Transicao invalida virou assignment. O State virou Strategy
// acidental: quem troca o estado e quem esta do lado de fora.

O que quebra

  • No fluxo felizDá para pular de Novo para Enviado sem pagar — transição inválida virou uma linha.
  • Na auditoriaNão há registro de quem autorizou a mudança; o histórico da máquina some.
  • No testeÉ preciso lembrar de setar estados 'impossíveis' para cobrir bugs que a máquina deveria tornar incompiláveis ou impossíveis.

A saída: Esconda o ponteiro: só os estados concretos chamam mudarPara. O cliente chama avancar()/cancelar() e nunca escolhe o sucessor — senão virou Strategy com nome errado.

Do cheiro ao padrão

Um passo por vez, com o motivo de cada um.

O cheiro: O mesmo if (status === ...) repetido em cada método, e nenhum lugar sabendo quais transições são válidas.

Como está. O mesmo if (status === ...) repetido em cada método, e nenhum lugar sabendo quais transições são válidas.

class Pedido {
  status = "novo";

  pagar() {
    if (this.status === "cancelado") throw new Error("cancelado");
    if (this.status === "pago") throw new Error("ja pago");
    this.status = "pago";
  }

  enviar() {
    if (this.status !== "pago") throw new Error("nao pago");
    this.status = "enviado";
  }

  cancelar() {
    // Esqueceram de checar "enviado". Pedido entregue vira cancelado.
    if (this.status === "cancelado") throw new Error("ja cancelado");
    this.status = "cancelado";
  }
}
08Cuidado

Armadilhas

  1. State para máquina de dois estados

    Ativo/inativo com uma regrinha por lado não justifica interface + duas classes + contexto delegando. Um boolean com if é mais legível e mais barato. O padrão paga a partir de 3+ estados com comportamento realmente distinto por operação.

  2. A máquina fica invisível

    Com as transições distribuídas pelas classes, ninguém vê a máquina inteira: para saber se Pago pode virar Cancelado é preciso ler Pago.cancelar(). Mantenha um diagrama da máquina junto do código (ou um teste que percorre todas as transições) — senão cada dev inventa uma transição nova sem perceber conflito.

  3. Estados compartilhados com dados de instância

    Uma otimização comum é reusar instâncias de estado (singleton por estado, já que muitos não têm campos). Mas se um estado guarda dados da transação atual (tentativas, timestamps), compartilhá-lo entre contextos mistura dados de pedidos diferentes — um bug de concorrência clássico. Ou o estado é stateless e compartilhável, ou é criado por contexto: nunca os dois.

09Decisão

Quando usar × quando evitar

Use quando

3
  • O comportamento do objeto depende fortemente do estado, com 3+ estados e regras diferentes por operação.
  • Os mesmos switch (status) se repetem em vários métodos — a máquina está espalhada pelo código.
  • As transições têm regras próprias e você quer cada uma explícita e testável isoladamente.

Evite quando

2
  • Dois estados e meia dúzia de linhas — um boolean com if é mais honesto que quatro classes.
  • Máquinas grandes, planas e dirigidas por eventos — uma tabela de transições declarativa ou uma lib de statecharts expressa melhor.

Testar o que ficou

3 armadilhas de State viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.

Onde isto aparece de verdade

Você já usa isto, talvez sem saber o nome.

  • XStateA biblioteca inteira é este padrão: estados nomeados, transições declaradas, comportamento que muda com o estado.
  • O ciclo de uma PromisePendente, resolvida e rejeitada respondem diferente ao mesmo .then() — e a transição é de mão única.

O padrão em uma linha

Sem nome de domínio, sem cerimônia. Só a ideia.

// Comportamento muda com o estado atual.
pedido.estado.pagar(pedido); // delega

O que ele cobra

1 salto a mais entre a chamada e o efeito.

  • Uma classe por estado, mais a máquina que gerencia as transições entre elas
  • Espalhar o comportamento por várias classes dificulta ver o fluxo completo num lugar só

Não vale se: há dois ou três estados com pouca lógica — um enum e um switch se leem melhor que uma classe por estado.

Costuma ser confundido com

Onde isto aparece

Continue por aqui