Pular para o conteúdo

ADR-0027 Princípios de engenharia

Padrão da empresa para a forma do código — KISS primeiro, YAGNI depois, SOLID onde o código tem uma razão real para mudar.

Decisão
KISS, YAGNI, SOLID, por esta ordemConstruir a app
Estado
Aceite
Data
Âmbito
Predefinição da empresa para todas as novas apps

Os ADRs 0001–0025 escolhem as ferramentas, e o ADR-0026 diz onde vivem os ficheiros. Nenhum diz quanta estrutura deve ter o código dentro deles, pelo que isso volta a ser decidido em cada revisão, por gosto.

  • Muitos produtos, uma equipa: quem muda de produto deve encontrar as mesmas formas — onde vive uma regra, onde se vão buscar os dados, onde vale a pena ter uma interface
  • Os princípios já cá estão, caso a caso: os hooks são donos dos dados e do estado e os componentes renderizam (ADR-0018); uma só fronteira de adapter para os fornecedores de modelos, passada como parâmetro (ADR-0025); os plugins do Better Auth são opt-in por produto (ADR-0009); Zustand só quando aparece mesmo estado partilhado entre árvores (ADR-0012); um registo partilhado de componentes só quando dois produtos duplicam um componente (ADR-0011)
  • Estrutura a mais é barata de escrever e cara de manter. As ferramentas de código com IA escrevem depressa e tendem a produzir mais camadas, opções e wrappers do que a tarefa pede; quem revê precisa de uma regra para onde apontar

O que está em aberto: que princípios nomeamos e qual ganha quando discordam.

Prós: nada para manter; cada equipa usa o seu próprio critério. Contras: a mesma discussão em cada pull request, com um resultado diferente por revisor; nada para entregar a quem chega ou a uma ferramenta de IA.

2. Uma arquitetura obrigatória (Clean / Hexagonal)

Seção intitulada “2. Uma arquitetura obrigatória (Clean / Hexagonal)”

Camadas fixas em todo o lado: entidades, casos de uso, repositórios, ports e adapters.

Prós

  • Todas as bases de código têm as mesmas pastas e as mesmas fronteiras
  • As regras de negócio testam-se sem infraestrutura

Contras

  • Uma interface com uma só implementação, para cada classe — cerimónia que pesa mais do que o CRUD que a maioria das nossas apps é
  • Contraria escolhas que já fizemos: o Drizzle é SQL-first e usado diretamente (ADR-0008), e os testes de integração correm em Postgres e Valkey reais (ADR-0014), pelo que uma camada de repositórios que existe para ser simulada não traz nada
  • Mais ficheiros para ler até encontrar a linha que faz o trabalho

3. Princípios curtos e conhecidos, com ordem de prioridade

Seção intitulada “3. Princípios curtos e conhecidos, com ordem de prioridade”

KISS, YAGNI e SOLID, com uma regra para os conflitos.

Prós

  • Nomes conhecidos: ensinados em todo o lado, pesquisáveis e entendidos pelas ferramentas de IA sem explicação
  • Curtos o suficiente para lembrar e para citar num comentário de revisão
  • A ordem resolve o conflito habitual — o SOLID a pedir uma abstração que o YAGNI diz ainda não ser precisa

Contras

  • “Simples” é um juízo, não uma medida; quem faz cumprir é a revisão de código, não um linter
  • O SOLID foi escrito para código orientado a objetos com classes e precisa de ser traduzido para funções, hooks e componentes
  • Menos uniforme do que uma arquitetura obrigatória: dois produtos podem ter aspetos diferentes

KISS, depois YAGNI, depois SOLID — aplicados por esta ordem — são o padrão da empresa para todo o código novo.

Em concreto:

  • KISS — o desenho mais simples que resolve o problema por inteiro. Preferir, por ordem: o que a base de código já tem, a plataforma ou a biblioteca padrão, uma dependência que estes ADRs já escolheram e só depois código novo. Legível ganha a engenhoso; uma linha que é preciso decifrar não é simples
  • YAGNI — construir para o requisito que temos. Nenhuma opção, flag de configuração, wrapper ou ponto de extensão até existir um segundo caso real. Extrair uma abstração partilhada quando o segundo caso chega, não antes. Apagar o código que não é usado em vez de o guardar “para depois” — o git lembra-se
  • SOLID — onde o código tem uma razão real para mudar ou uma segunda implementação real, nunca como estrutura especulativa
  • Backend:
    • Responsabilidade única: um módulo ou função é dono de uma só razão para mudar. Uma server function ou um handler de rota da API (ADR-0036) faz parse, autoriza e chama uma função da funcionalidade; a validação é Zod (ADR-0005), as regras de negócio são funções simples em server/services/ (ADR-0026), a persistência é Drizzle (ADR-0008)
    • Aberto/fechado: acrescentar comportamento acrescentando um caso (um handler, uma estratégia, uma linha de tabela) em vez de editar ramos que funcionam — quando existe uma segunda variante
    • Substituição de Liskov: todas as implementações de uma interface cumprem o mesmo contrato: entradas, saídas, erros, efeitos secundários. Nenhuma implementação que lança “not supported”
    • Segregação de interfaces: as interfaces são pequenas e desenhadas por quem as chama. Dividir um serviço quando quem o chama usa metades diferentes
    • Inversão de dependências: a lógica de negócio recebe o que atravessa uma fronteira real — o db do Drizzle, o cliente Valkey, clientes de APIs de terceiros, o relógio — como parâmetros, para que um teste possa passar os seus. O código interno puro importa código concreto diretamente
  • UI:
    • Responsabilidade única: um componente renderiza ou orquestra. O carregamento de dados e a lógica de estado vivem em hooks (ADR-0012); os componentes apresentacionais mantêm-se puros
    • Aberto/fechado: estender por composição — children, slots, props render, variantes cva (ADR-0011) — e não duplicando um componente ou acumulando flags booleanas
    • Substituição de Liskov: um wrapper ou uma variante aceita e reencaminha as props do componente base (ref, className, atributos ARIA, handlers de eventos) e comporta-se da mesma forma
    • Segregação de interfaces: passar os campos de que o componente precisa, não o objeto de domínio inteiro
    • Inversão de dependências: os componentes recebem clientes de API, stores e serviços por props, contexto ou hooks, nunca importando um singleton concreto no fundo da árvore
  • Quando discordam, ganha o desenho mais simples, e o pull request regista o compromisso numa linha
  • O que a simplicidade nunca remove: a validação nas fronteiras do sistema (ADR-0005), a autenticação e a autorização (ADR-0009), os limites de pedidos (ADR-0010), o tratamento de erros que evita perda de dados, a acessibilidade e os testes do ADR-0014
  • Cumprido na revisão de código. Perante qualquer abstração, opção ou dependência nova, quem revê faz uma pergunta: que requisito atual precisa disto? Sem resposta, não há merge

Os fatores decisivos: nomes que qualquer programador e ferramenta de IA já conhece, uma lista curta o suficiente para citar numa revisão e uma ordem explícita que impede o SOLID de se tornar um motivo para acrescentar camadas. Aceitamos que “simples” continue a ser um juízo feito na revisão.

Mais fácil:

  • As discussões em revisão têm um vocabulário comum e uma regra de desempate
  • Menos código para ler, testar e atualizar: as abstrações aparecem quando um segundo caso as justifica
  • As mesmas formas em todos os produtos — handlers finos, regras em módulos simples, hooks donos do estado, componentes que renderizam
  • Os testes precisam de poucos mocks: as dependências chegam como parâmetros e a base de dados é a real (ADR-0014)

Mais difícil:

  • Exige critério; dois revisores podem continuar a discordar sobre o que é “o mais simples”
  • Extrair uma abstração tarde obriga a voltar a mexer na primeira cópia — um custo pequeno e conhecido
  • Manter a linha perante código gerado que chega com camadas que ninguém pediu

Do que abdicamos: da uniformidade de uma arquitetura obrigatória e do DRY como regra própria. Aceite — a duplicação é mais barata do que a abstração errada, e o YAGNI já diz quando extrair.