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.
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.
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.
- 01Contrato 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.
- 02Seleçã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.
- 03Custo 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.
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.
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.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.
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.
- 01Política padrão
Aprovador habilitado decide sobre valor abaixo do limiar, enquanto ator sem papel apropriado é negado.
- 02Política de valor elevado
Um aprovador comum é negado e um aprovador sênior pode operar, segundo configuração confiável.
- 03Transições válidas e ilegais
Rascunho → pendente → aprovado/recusado. Submeter um aprovado e aprovar novamente deve produzir erro verificável.
- 04Negativas 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.
- 05Equivalê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.
- 06Limite 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.
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.
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.
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.
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.
CATÁLOGO CLÁSSICO / 23 GOF
Voltar à biblioteca e comparar outros mecanismos.
O catálogo organiza as três famílias e seus contraindicações. Novos estudos individuais serão adicionados quando tiverem problema, código e testes próprios — nunca como placeholders.
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.
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.
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.
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.