Design de Base de Código
Projete módulos profundos: muito comportamento atrás de uma interface pequena, colocada em uma junta (seam) limpa, testável através dessa interface. Use esta linguagem e estes princípios sempre que código estiver sendo projetado ou reestruturado. O objetivo é alavancagem para chamadores, localidade para mantenedores e testabilidade para todos.
Glossário
Use estes termos exatamente: não substitua por "componente", "serviço", "API" ou "fronteira". Linguagem consistente é o ponto principal.
Módulo: qualquer coisa com uma interface e uma implementação. Deliberadamente agnóstico em escala: uma função, classe, pacote ou fatia que atravessa camadas. Evite: unidade, componente, serviço.
Interface: tudo que um chamador precisa saber para usar o módulo corretamente: a assinatura de tipo, mas também invariantes, restrições de ordenação, modos de erro, configuração necessária e características de desempenho. Evite: API, assinatura (muito estreitos, referem-se apenas à superfície de tipo).
Implementação: o que está dentro de um módulo, seu corpo de código. Distinto de Adaptador: uma coisa pode ser um adaptador pequeno com uma implementação grande (um repositório Postgres) ou um adaptador grande com uma implementação pequena (um fake em memória). Use "adaptador" quando a junta for o tópico; "implementação" caso contrário.
Profundidade: alavancagem na interface. A quantidade de comportamento que um chamador (ou teste) pode exercer por unidade de interface que precisa aprender. Um módulo é profundo quando uma grande quantidade de comportamento está atrás de uma interface pequena, raso quando a interface é quase tão complexa quanto a implementação.
Seam (Junta) (Michael Feathers): um lugar onde você pode alterar o comportamento sem editar naquele lugar; a localização onde a interface de um módulo vive. Onde colocar a junta é uma decisão de design em si, distinta do que fica atrás dela. Evite: fronteira (sobrecarregada com o contexto delimitado de DDD).
Adaptador: uma coisa concreta que satisfaz uma interface em uma junta. Descreve o papel (qual slot preenche), não a substância (o que está dentro).
Alavancagem: o que os chamadores obtêm da profundidade. Mais capacidade por unidade de interface que aprendem. Uma implementação compensa em N locais de chamada e M testes.
Localidade: o que os mantenedores obtêm da profundidade. Mudança, bugs, conhecimento e verificação concentram-se em um lugar em vez de se espalhar pelos chamadores. Corrige uma vez, corrigido em todo lugar.
Profundo vs Raso
Módulo profundo = interface pequena + muita implementação:
┌─────────────────────┐
│ Interface Pequena │ ← Poucos métodos, parâmetros simples
├─────────────────────┤
│ │
│ Implementação │ ← Lógica complexa escondida
│ Profunda │
│ │
└─────────────────────┘
Módulo raso = interface grande + pouca implementação (evite):
┌─────────────────────────────────┐
│ Interface Grande │ ← Muitos métodos, parâmetros complexos
├─────────────────────────────────┤
│ Implementação Fina │ ← Apenas repassa
└─────────────────────────────────┘
Ao projetar uma interface, pergunte:
- Posso reduzir o número de métodos?
- Posso simplificar os parâmetros?
- Posso esconder mais complexidade internamente?
Princípios
- Profundidade é uma propriedade da interface, não da implementação. Um módulo profundo pode ser internamente composto de partes pequenas, simuláveis e substituíveis; elas simplesmente não fazem parte da interface. Um módulo pode ter juntas internas (privadas à sua implementação, usadas por seus próprios testes) bem como a junta externa em sua interface.
- O teste de exclusão. Imagine deletar o módulo. Se a complexidade desaparecer, era um repasse. Se a complexidade reaparecer em N chamadores, estava ganhando seu lugar.
- A interface é a superfície de teste. Chamadores e testes cruzam a mesma junta. Se você quiser testar além da interface, o módulo provavelmente tem a forma errada.
- Um adaptador significa uma junta hipotética. Dois adaptadores significam uma junta real. Não introduza uma junta a menos que algo realmente varie através dela.
Projetando para testabilidade
Interfaces boas tornam o teste natural:
-
Aceite dependências, não as crie.
// Testável function processOrder(order, paymentGateway) {} // Difícil de testar function processOrder(order) { const gateway = new StripeGateway(); } -
Retorne resultados, não produza efeitos colaterais.
// Testável function calculateDiscount(cart): Discount {} // Difícil de testar function applyDiscount(cart): void { cart.total -= discount; } -
Área de superfície pequena. Menos métodos = menos testes necessários. Menos parâmetros = configuração de teste mais simples.
Relacionamentos
- Um Módulo tem exatamente uma Interface (a superfície que apresenta aos chamadores e testes).
- Profundidade é uma propriedade de um Módulo, medida contra sua Interface.
- Uma Seam (Junta) é onde a Interface de um Módulo vive.
- Um Adaptador fica em uma Seam (Junta) e satisfaz a Interface.
- Profundidade produz Alavancagem para chamadores e Localidade para mantenedores.
Enquadramentos rejeitados
- Profundidade como razão entre linhas de implementação e linhas de interface (Ousterhout): recompensa o aumento da implementação. Usamos profundidade-como-alavancagem em vez disso.
- "Interface" como a palavra-chave
interfacedo TypeScript ou os métodos públicos de uma classe: muito estreito: interface aqui inclui todo fato que um chamador precisa saber. - "Fronteira": sobrecarregada com o contexto delimitado de DDD. Diga seam (junta) ou interface.
Indo mais fundo
- Aprofundando um cluster dadas suas dependências, veja DEEPENING.md: categorias de dependência, disciplina de juntas e teste de substituir-não-camadas.
- Explorando interfaces alternativas, veja DESIGN-IT-TWICE.md: inicie subagentes paralelos para projetar a interface de várias maneiras radicalmente diferentes, depois compare em profundidade, localidade e posicionamento da junta.