Pular para o conteúdo
Vamos conversar

ATLAS / ESTUDO DE DESIGN PATTERNS / FABRÍCIO CASTRO

Strategy decide a regra.
State decide a transição.

Dois padrões comportamentais, duas perguntas diferentes. Vamos examinar uma aprovação de compra fictícia, com políticas de alçada e estados de solicitação, e demonstrar o que o padrão resolve — e o que continua exigindo autorização, banco de dados e testes.

01 / CASO DIDÁTICO ORIGINAL

A mesma compra tem duas dimensões independentes.

Uma solicitação possui valor, organização, solicitante, versão e estado. A regra que decide quem pode aprovar depende de uma política de alçada; os eventos que ela aceita dependem do estado. Misturar ambos no mesmo switch parece rápido, até que cada mudança afeta autorização e ciclo de vida ao mesmo tempo.

DIMENSÃO A / POLÍTICA DE ALÇADA

Qual algoritmo de decisão se aplica?

Valores abaixo de um limite fictício usam aprovador comum ou sênior; valores maiores precisam de aprovador sênior. O limite é configuração de negócio validada, não um valor fixo a aplicar a qualquer cliente.

Strategy: seleciona uma política específica sem alterar o fluxo de estados.

DIMENSÃO B / CICLO DE VIDA

Qual evento é válido neste estado?

Um pedido em rascunho pode ser enviado. Pendente pode ser aprovado ou recusado. Aprovado e recusado são terminais neste exemplo. Uma regra de reabertura futura exigiria nova especificação, não um comportamento inventado.

State: valida a transição sem escolher quem tem alçada.

02 / STRATEGY — ALGORITMO INTERCAMBIÁVEL

Trocar política não significa trocar estado.

Uma interface `ApprovalStrategy` define `canApprove(actor)`. Estratégias `standardApproval` e `highValueApproval` avaliam permissões segundo políticas diferentes. O caso de uso seleciona a estratégia por configuração confiável e não pela requisição enviada pelo usuário.

// O domínio NÃO deixa o solicitante selecionar sua própria alçada.
const policy = selectPolicy(purchase, trustedConfig);
if (!policy.canApprove(authenticatedActor)) {
  throw new Error("Alçada insuficiente");
}
// Strategy responde: qual algoritmo/política usar?

Trecho simplificado do módulo didático `approval-patterns.ts`. O valor de alçada é uma regra de exemplo, não política de empresa real.

  1. 01
    Contrato estável

    O caso de uso conhece apenas a operação `canApprove`, não qual implementação está ativa. Isso faz sentido quando políticas realmente variam sem alterar o restante do fluxo.

  2. 02
    Seleção controlada

    O servidor escolhe a política segundo dados confiáveis e vigentes. Receber do frontend o nome da estratégia abriria um bypass grave de autorização.

  3. 03
    Custo da abstração

    Se só existem duas condições pequenas e estáveis, funções puras podem ser suficientes. Não há requisito arquitetural de classes para um Strategy válido em TypeScript.

03 / STATE — CICLO DE VIDA

O comportamento depende da situação atual.

O módulo didático representa `DraftState`, `PendingState`, `ApprovedState` e `RejectedState`, cada um com operações de transição. A aplicação não pode aprovar um pedido já aprovado nem recusar um rascunho sem envio; o estado decide o conjunto de eventos legais.

// O estado atual decide quais eventos são válidos.
nextApprovalStatus("draft", "submit");   // "pending"
nextApprovalStatus("pending", "approve"); // "approved"
nextApprovalStatus("approved", "approve"); // lança erro
// State responde: o que este objeto pode fazer agora?

As classes demonstram o mecanismo, mas não armazenam transações nem substituem a versão persistida.

BENEFÍCIO

Regra localizada por estado.

Cada estado conhece suas próprias transições. Um fluxo com dezenas de eventos e comportamento por estado pode ganhar legibilidade ao separar implementações e testes por estado.

CUSTO

Mais objetos e lugares para investigar.

Com apenas quatro estados e três eventos, a estrutura pode custar mais do que entrega. Alterar invariantes globais exige atenção para não duplicá-las de modo incoerente entre classes.

04 / CONTRAEXEMPLO — SIMPLICIDADE COMO OPÇÃO

Um switch ou condições claras podem ser a melhor escolha.

Design Patterns não são uma lista de objetivos para preencher. A função abaixo resolve o mesmo pequeno grafo de estados sem hierarquia. Para poucas transições estáveis, ela pode ser mais legível, fácil de testar e barata de manter.

// Exemplo menor, sem classes State — igualmente legítimo.
function nextStatusWithoutState(status, event) {
  if (status === "draft" && event === "submit") return "pending";
  if (status === "pending" && event === "approve") return "approved";
  if (status === "pending" && event === "reject") return "rejected";
  throw new Error("Transição inválida");
}

Alternativa também presente no módulo didático. A equivalência de comportamentos é verificada por testes; não há razão para declarar uma implementação universalmente superior.

CRITÉRIO DE REFATORAÇÃO

Mude quando houver uma dor observável.

Use o custo de novas transições, incidência de bugs, cobertura de testes e facilidade de leitura para escolher. A existência de classes não demonstra evolução arquitetural por si só.

FEW STATESUma função bem nomeada e casos de teste bastam.

NEW BEHAVIORSe cada estado passa a coordenar múltiplos comportamentos, State pode isolar variações.

POLICY ONLYSe a mudança ocorre na alçada, Strategy é candidato; State não resolve esse eixo.

REMOTE FAILURESe o problema é entrega duplicada ao ERP, estudar Outbox/Idempotência, não State.

05 / RESTRIÇÕES NÃO NEGOCIÁVEIS

Um padrão não autoriza ninguém. Nem garante consistência.

O serviço real precisa verificar identidade autenticada, organização, políticas de alçada e conflito de interesses. A transição também precisa ser persistida de modo atômico quando há concorrência. O laboratório abaixo é puro TypeScript; não demonstra isolamento transacional em PostgreSQL.

const checked = decidePurchase(
  purchase, // estado pendente, valor e versão observada
  actor,    // identidade previamente autenticada no servidor
  "approve",
  { highValueThresholdCents: 100_000 }, // configuração confiável
);
// No banco real: persistir usando WHERE id/tenant/version,
// e exigir uma linha afetada; nunca confiar só no objeto em memória.
AUTORIZAÇÃO / FALHA NEGADA

Identidade e tenant vêm de contexto confiável.

Um atacante tenta passar outra organização no payload; o servidor deve ignorar ou rejeitar essa atribuição e aplicar o escopo autenticado. O estudo de funções puras contém guardas ilustrativas, mas não implementa um provedor de identidade real.

Teste obrigatório: negar autoaprovação, acesso entre tenants e alçada insuficiente.

BANCO / FALHA DE CONCORRÊNCIA

Controle de versão é compromisso transacional.

O código aumenta o campo `version`, porém dois processos podem ter lido a mesma versão. Somente uma escrita condicional no banco, com checagem de resultado, prova a exclusão de decisões incompatíveis. Um teste Jest em memória não comprova isso.

Próximo laboratório: executar duas transações PostgreSQL reais e verificar a restrição aplicada.

06 / CRITÉRIOS DE ACEITE REPRODUZÍVEIS

Especificar o esperado antes de alegar sucesso.

A suíte Jest do exemplo está versionada no repositório e exige comportamentos positivos e negativos. O status só pode ser PASS quando os testes forem realmente executados no commit correspondente.

  1. 01
    Política padrão

    Aprovador habilitado decide sobre valor abaixo do limiar, enquanto ator sem papel apropriado é negado.

  2. 02
    Política de valor elevado

    Um aprovador comum é negado e um aprovador sênior pode operar, segundo configuração confiável.

  3. 03
    Transições válidas e ilegais

    Rascunho → pendente → aprovado/recusado. Submeter um aprovado e aprovar novamente deve produzir erro verificável.

  4. 04
    Negativas de autorização

    Autoaprovação, tenant cruzado, valor inválido e versão inválida devem falhar; erro não concede efeito de negócio.

  5. 05
    Equivalência do caminho simples

    Comparar todas as combinações de estados e eventos e comprovar que a função direta retorna resultado ou erro equivalente às classes State.

  6. 06
    Limite da prova

    Jest não reproduz transações reais, retry do ERP nem funcionamento da UI. Playwright, banco real e testes de integração permanecem tarefas diferentes.

07 / REVISÃO DE DESENHO

Escolher a abstração é comparar seus custos.

Strategy tende a ser útil quando uma política muda independentemente do caso de uso; State quando há comportamento suficiente por estado para justificar a estrutura. Separar responsabilidade é importante, mas não autoriza classes demais nem regras espalhadas.

PREFERIR STRATEGY

Variantes de algoritmo mudam frequentemente.

Alçadas por jurisdição, produto ou contrato exigem decisões diferentes, isoláveis e testáveis, sem alterar o ciclo de vida. Evite transformar a estratégia no sistema de autorização: a aplicação ainda precisa controlar a origem da política.

PREFERIR STATE

O ciclo possui múltiplos comportamentos por estado.

Máquina de estados com guardas e ações extensas pode justificar objetos especializados; ainda será necessário validar invariantes globais e persistência consistente.

PREFERIR SIMPLICIDADE

O grafo é pequeno e previsível.

Função pura com mapeamento explícito e testes pode resolver com menos custo cognitivo. Refatorar para State é opcional, conforme manutenção demonstrada.

EVITAR CONCLUSÕES FALSAS

Ganho de design não é ganho de produção.

Não afirmar throughput, latência, observabilidade, segurança ou qualidade real sem fonte e execução. Este exemplo serve para aprender a isolar responsabilidades, não para substituir a revisão de uma solução empresarial.

08 / REFERÊNCIAS E TRAÇABILIDADE

O exemplo é original. As fontes permitem confrontá-lo.

O código fica em `src/lib/atlas/examples/approval-patterns.ts`, com suíte Jest correspondente. Os fragmentos acima ilustram as decisões mais importantes, e a implementação integral é didática. Nunca representar este laboratório como sistema comercial desenvolvido ou executado em produção.

GOF / REFERÊNCIA HISTÓRICA

Design Patterns: Elements of Reusable Object-Oriented Software.

Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides (1994). As intenções clássicas de Strategy e State foram adaptadas para um contexto didático TypeScript sem copiar código do livro.

DOCUMENTAÇÃO DE LINGUAGEM

TypeScript Handbook.

Consulte tipos discriminados, narrowing e contratos. A tipagem ajuda a expressar domínio, mas não garante autorização e integridade no banco.

Consultar guia oficial

Estado da entrega: código e casos de teste criados na branch. Resultados Jest, typecheck, Playwright e homologação visual requerem execução real; nenhuma execução foi presumida.

Continue explorando.Engenharia de software ↗
Vamos conversar?

Explore o portfólio

26 páginas encontradas · Esc para fechar