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.
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.
- Cliente
- avancar()Pedido (contexto)
- delega ao estado atualEstado: Novo
- transicionaEstado: Pago
- transicionaEstado: Enviado
Navegue pelas camadas
Camada 1/3Contexto
use ← →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.
Estrutura
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"); }
}from abc import ABC, abstractmethod
class EstadoPedido(ABC):
@abstractmethod
def avancar(self, pedido) -> None: ...
@abstractmethod
def cancelar(self, pedido) -> None: ...
class Pedido:
def __init__(self):
self.estado: EstadoPedido = Novo()
def avancar(self): self.estado.avancar(self)
def cancelar(self): self.estado.cancelar(self)
class Novo(EstadoPedido):
def avancar(self, p):
print("Pagamento confirmado")
p.estado = Pago() # o ESTADO decide a transicao
def cancelar(self, p):
print("Cancelado sem custo")
class Pago(EstadoPedido):
def avancar(self, p):
print("Despachado para o cliente")
p.estado = Enviado()
def cancelar(self, p):
print("Cancelado com estorno")
class Enviado(EstadoPedido):
def avancar(self, p):
print("Ja enviado - nada a avancar")
def cancelar(self, p):
print("Nao e possivel cancelar apos envio")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.
- 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
Uma classe por estado
Cada estado implementa as operações — inclusive para recusar. Recusar também é comportamento explícito.
- 3
Delegar no contexto
O Pedido (ou Player) guarda o estado atual e só repassa. Nenhum `if (status === ...)` no contexto.
- 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";
}
}Armadilhas
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.
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.
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.
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); // delegaO 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.