Padrão Comportamental (GoF)

State

Permite que um objeto altere seu comportamento quando seu estado interno muda — como se o objeto mudasse de classe em tempo de execução. Cada estado possível é encapsulado em sua própria classe, e as transições fazem parte do design.

Intenção

Encapsular cada estado possível de um objeto em sua própria classe e delegar o comportamento dependente de estado ao objeto de estado corrente. Do ponto de vista do contexto, o objeto parece "mudar de classe" quando muda de estado — mas na realidade ele troca o objeto de estado que detém o comportamento.

Catalogado pelo GoF (1994) como padrão comportamental, o State é a solução elegante para máquinas de estado finitas: substitui condicionais extensas (switch ou if/else) que verificam o estado atual por uma estrutura onde cada estado conhece seus próprios comportamentos válidos e suas transições permitidas.

Problema

Considere o workflow de um documento editorial: rascunho, em revisão e publicado. Cada estado permite ações diferentes: um rascunho pode ser enviado para revisão, mas não publicado diretamente; um documento em revisão pode ser aprovado (vai para publicado) ou rejeitado (volta para rascunho); um documento publicado não pode retroceder.

A abordagem ingênua codifica todas essas regras em condicionais dentro do contexto:

// Abordagem ingênua — NÃO faça isso:
class Documento {
  private estado: string = "rascunho";

  publicar(): void {
    if (this.estado === "rascunho") {
      throw new Error("Rascunho nao pode ser publicado diretamente.");
    } else if (this.estado === "revisao") {
      this.estado = "publicado";
      console.log("Documento publicado.");
    } else if (this.estado === "publicado") {
      throw new Error("Ja publicado.");
    }
  }

  enviarParaRevisao(): void {
    if (this.estado === "rascunho") {
      this.estado = "revisao";
    } else {
      throw new Error("So rascunhos podem ir para revisao.");
    }
  }
  // Cada novo estado ou ação cresce todos os if/else acima.
}

Adicionar um novo estado (ex.: arquivado) exige abrir todos os métodos e adicionar mais uma ramificação — violação do Princípio Aberto/Fechado. O State resolve isso extraindo cada estado para sua própria classe.

Solução

O State organiza o código em três participantes:

  1. Context: mantém uma referência ao estado atual (EstadoDocumento) e delega as operações a ele. Expõe um método interno para que os estados possam solicitar a troca de estado.
  2. State (interface): declara os métodos que representam as operações dependentes de estado. Todos os estados concretos implementam esta interface.
  3. ConcreteState: implementa o comportamento correspondente a um estado específico do contexto e pode disparar transições chamando o método de troca de estado no contexto.

Distinção-chave: State vs Strategy

State e Strategy são estruturalmente idênticos — ambos têm um contexto que delega para um objeto intercambiável que implementa uma interface comum. A diferença está na intenção e no controle das transições:

  • Strategy: o cliente escolhe e injeta o algoritmo. O contexto não sabe qual estratégia está ativa e não se importa — não há transições automáticas. Algoritmos não se conhecem entre si.
  • State: as transições são parte do design e acontecem automaticamente. Os estados se conhecem: um estado sabe para quais outros estados pode transitar. O cliente raramente troca o estado diretamente — o estado atual faz isso em resposta a eventos.

Estrutura

         «interface»
        EstadoDocumento
  ┌──────────────────────────────────────┐
  │ + publicar(ctx: Documento): void     │
  │ + enviarParaRevisao(ctx): void       │
  │ + rejeitar(ctx: Documento): void     │
  └──────────────────────────────────────┘
              ▲
   ┌──────────┼──────────────────────┐
   │          │                      │
EstadoRascunho EstadoRevisao   EstadoPublicado
(ConcreteState) (ConcreteState) (ConcreteState)
  enviar OK    publicar OK      todas: erro
  publicar err rejeitar OK


         Documento (Context)
  ┌──────────────────────────────────────┐
  │ - estado: EstadoDocumento            │
  │ + publicar(): void                   │
  │ + enviarParaRevisao(): void          │
  │ + rejeitar(): void                   │
  │ + transicionarPara(e): void [pkg]    │
  └──────────────────────────────────────┘
              │ delega para
              ▼
        EstadoDocumento (corrente)


Fluxo de transição:

  doc.enviarParaRevisao()
    → EstadoRascunho.enviarParaRevisao(doc)
      → doc.transicionarPara(new EstadoRevisao())

  doc.publicar()
    → EstadoRevisao.publicar(doc)
      → doc.transicionarPara(new EstadoPublicado())

Exemplos de código

Exemplo 1 — Workflow de documento (rascunho → revisão → publicado)

Implementação completa com interface de estado, estados concretos que controlam as próprias transições e um contexto que delega sem condicionais.

// ── State interface ───────────────────────────────────────────
interface EstadoDocumento {
  publicar(doc: Documento): void;
  enviarParaRevisao(doc: Documento): void;
  rejeitar(doc: Documento): void;
  nome(): string;
}

// ── Context ───────────────────────────────────────────────────
class Documento {
  private estado: EstadoDocumento;

  constructor(private readonly titulo: string) {
    this.estado = new EstadoRascunho();
  }

  // Chamado pelos estados concretos — não pelo cliente diretamente.
  transicionarPara(novoEstado: EstadoDocumento): void {
    console.log(`  [${this.titulo}] ${this.estado.nome()} -> ${novoEstado.nome()}`);
    this.estado = novoEstado;
  }

  publicar(): void           { this.estado.publicar(this); }
  enviarParaRevisao(): void  { this.estado.enviarParaRevisao(this); }
  rejeitar(): void           { this.estado.rejeitar(this); }

  getNomeEstado(): string    { return this.estado.nome(); }
}

// ── ConcreteStates ────────────────────────────────────────────
class EstadoRascunho implements EstadoDocumento {
  publicar(doc: Documento): void {
    throw new Error("Rascunho nao pode ser publicado sem revisao.");
  }
  enviarParaRevisao(doc: Documento): void {
    doc.transicionarPara(new EstadoRevisao());
  }
  rejeitar(doc: Documento): void {
    throw new Error("Rascunho nao pode ser rejeitado.");
  }
  nome(): string { return "Rascunho"; }
}

class EstadoRevisao implements EstadoDocumento {
  publicar(doc: Documento): void {
    doc.transicionarPara(new EstadoPublicado());
  }
  enviarParaRevisao(doc: Documento): void {
    throw new Error("Ja esta em revisao.");
  }
  rejeitar(doc: Documento): void {
    doc.transicionarPara(new EstadoRascunho());
  }
  nome(): string { return "Em Revisao"; }
}

class EstadoPublicado implements EstadoDocumento {
  publicar(doc: Documento): void {
    throw new Error("Ja publicado.");
  }
  enviarParaRevisao(doc: Documento): void {
    throw new Error("Documento publicado nao retorna para revisao.");
  }
  rejeitar(doc: Documento): void {
    throw new Error("Documento publicado nao pode ser rejeitado.");
  }
  nome(): string { return "Publicado"; }
}

// ── Uso ──────────────────────────────────────────────────────
const doc = new Documento("Guia de Design Patterns");

doc.enviarParaRevisao();
// [Guia de Design Patterns] Rascunho -> Em Revisao

doc.rejeitar();
// [Guia de Design Patterns] Em Revisao -> Rascunho

doc.enviarParaRevisao();
// [Guia de Design Patterns] Rascunho -> Em Revisao

doc.publicar();
// [Guia de Design Patterns] Em Revisao -> Publicado

console.log("Estado final:", doc.getNomeEstado());
// Estado final: Publicado

try {
  doc.publicar(); // ja publicado
} catch (e) {
  console.log("Erro esperado:", (e as Error).message);
  // Erro esperado: Ja publicado.
}

Exemplo 2 — Singleton de estado: objetos de estado compartilhados

Quando os estados não têm dados próprios (são stateless), eles podem ser compartilhados como singletons — economizando alocações. Esta é uma otimização mencionada pelo GoF e ilustra a relação entre State e Singleton.

// Estados stateless podem ser compartilhados — instâncias únicas por tipo.
// Aqui usamos objetos literais (mais leve que classes em TS para casos simples).

interface EstadoPlayer {
  play(player: MediaPlayer): void;
  pause(player: MediaPlayer): void;
  stop(player: MediaPlayer): void;
  nome(): string;
}

// Estado singleton como objeto literal tipado.
const Parado: EstadoPlayer = {
  play(player) { player.transicionarPara(Tocando); },
  pause(_p)    { /* ignorar — ja parado */ },
  stop(_p)     { /* ja parado */ },
  nome()       { return "Parado"; },
};

const Tocando: EstadoPlayer = {
  play(_p)     { /* ja tocando */ },
  pause(player){ player.transicionarPara(Pausado); },
  stop(player) { player.transicionarPara(Parado); },
  nome()       { return "Tocando"; },
};

const Pausado: EstadoPlayer = {
  play(player) { player.transicionarPara(Tocando); },
  pause(_p)    { /* ja pausado */ },
  stop(player) { player.transicionarPara(Parado); },
  nome()       { return "Pausado"; },
};

class MediaPlayer {
  private estado: EstadoPlayer = Parado;

  transicionarPara(novoEstado: EstadoPlayer): void {
    console.log(`  ${this.estado.nome()} -> ${novoEstado.nome()}`);
    this.estado = novoEstado;
  }

  play():  void { this.estado.play(this); }
  pause(): void { this.estado.pause(this); }
  stop():  void { this.estado.stop(this); }

  getEstado(): string { return this.estado.nome(); }
}

// ── Uso ──────────────────────────────────────────────────────
const player = new MediaPlayer();
console.log("Inicio:", player.getEstado()); // Inicio: Parado

player.play();   //   Parado -> Tocando
player.pause();  //   Tocando -> Pausado
player.play();   //   Pausado -> Tocando
player.stop();   //   Tocando -> Parado
console.log("Fim:", player.getEstado()); // Fim: Parado

Quando usar

  • Quando um objeto muda radicalmente de comportamento dependendo do estado e há muitos estados possíveis: workflows (pedido, documento, ticket), players de mídia, conexões de rede (conectado/desconectado/reconectando), portas de comunicação.
  • Quando as transições de estado são complexas e têm regras: cada estado sabe de quais outros estados pode vir e para quais pode ir. Isso fica claro e testável quando encapsulado em classes de estado.
  • Para eliminar condicionais de estado espalhadas por vários métodos: se você tem if (this.estado === "x") repetido em dezenas de métodos, State extrai essa lógica para onde ela pertence — dentro de cada estado.

Quando evitar

  • Quando há apenas dois ou três estados simples e estáveis: um enum com uma condicional direta é mais legível do que criar toda a hierarquia de classes de Estado para uma máquina trivial.
  • Quando o comportamento não muda por estado: se todos os estados executam os mesmos métodos com a mesma lógica, o padrão não adiciona valor — você está criando classes vazias.

Prós e contras

Prós

  • Elimina grandes condicionais de estado espalhadas pelo código do contexto.
  • Cada estado é uma classe isolada — fácil de testar, entender e modificar sem afetar outros estados.
  • Novos estados podem ser adicionados sem alterar o contexto nem os estados existentes (Princípio Aberto/Fechado).
  • Transições explícitas dentro de cada estado tornam o fluxo legível e auditável.
  • Estados stateless podem ser compartilhados como singletons (otimização de memória).

Contras

  • Explosão de classes para máquinas de estado com muitos estados — cada estado vira uma classe.
  • A visão global das transições fica distribuída entre as classes de estado — mais difícil de enxergar o diagrama completo em um só lugar.
  • Para máquinas simples, o overhead de classes é desnecessário — uma tabela de transições ou uma enum pode ser suficiente.

Armadilhas comuns

1. Onde colocar a lógica de transição

A decisão mais delicada: quem dispara a transição de estado — o Context ou o ConcreteState? O GoF recomenda que os próprios estados disparem as transições, pois eles conhecem as regras de quando e para onde transitar. Isso distribui o conhecimento de transição para cada estado, tornando cada um independente. O risco: estados que se referenciam mutuamente criam acoplamento circular. Para evitar isso, passe o Context para o estado (como mostrado nos exemplos) em vez de instanciar os estados diretamente dentro de outros estados quando possível.

2. Explosão de classes para máquinas simples

Regra prática: se a máquina tem menos de quatro estados e as transições são simples, considere uma abordagem mais leve: uma enum de estado + um método de transição com um switch centralizado no Context. Reserve o padrão State completo para máquinas onde o comportamento por estado é substancialmente diferente e as transições têm lógica relevante.

3. Estados com dados vs estados stateless

Se um estado precisa armazenar dados próprios (ex.: contador de tentativas em um estado de "reconectando"), ele não pode ser compartilhado como singleton — cada instância de Context precisa da sua própria instância do estado. Estados stateless são a exceção, não a regra. Tente manter os dados do contexto no Context e deixar os estados como pura lógica de comportamento e transição.

4. Confundir State com Strategy

Estruturalmente idênticos, mas a diferença de intenção importa na prática. Se você está modelando transições automáticas e os "objetos" se conhecem para transitar entre si — é State. Se o cliente escolhe e injeta o comportamento externamente e não há transições entre "estados" — é Strategy. A pergunta de diagnóstico: "quem decide a próxima variação — o próprio objeto ou o cliente externo?". Se o próprio objeto decide, é State.

Padrões relacionados

O State interage com outros padrões comportamentais e de criação:

O Strategy é o "irmão estrutural" do State: ambos encapsulam comportamento variável em objetos intercambiáveis, mas Strategy é controlado externamente pelo cliente (sem transições automáticas) enquanto State controla suas próprias transições. O Singleton é usado como otimização quando os estados são stateless — cada estado concreto sem dados próprios pode ser compartilhado como instância única entre todos os contextos. O Observer pode ser combinado com State para notificar componentes externos sobre transições de estado — o Context age como Subject e notifica Observers quando o estado muda.