Trilha 3 · Do conceito à produção · ~22 min
Já expliquei o Public API Provider (aula Comunicação entre módulos: o padrão Public API Provider) para times inteiros, todo mundo concordou, documentamos na wiki com diagrama bonito. Três meses depois, auditei o código: 60+ imports furando fronteira. Não por má-fé — por sexta-feira à noite, deadline, "só dessa vez". Diretriz que depende de disciplina humana não é governança; é esperança. Ninguém acorda querendo violar a arquitetura; o acoplamento entra por mil pequenas decisões locais sob pressão, e o review humano não pega todas (o revisor também está cansado, também tem deadline).
A virada de chave da minha carreira como arquiteto foi parar de escrever regras em documentos e começar a escrever regras em código que roda no CI. Uma fitness function é isso: um teste automatizado cuja unidade sob teste não é uma função de negócio, é uma propriedade arquitetural ("nenhum módulo importa o interno de outro"). Se a propriedade é violada, o build quebra. A arquitetura passa a se defender sozinha — que é a única forma de arquitetura que sobrevive ao tempo e à pressão.
O conceito: governança que se auto-aplica
O termo fitness function vem de Building Evolutionary Architectures (Ford, Parsons, Kua). A ideia: definir, de forma objetiva e executável, o que significa "a arquitetura está saudável", e rodar essa verificação continuamente. Se você não consegue medir uma propriedade arquitetural automaticamente, você não consegue defendê-la — ela vai degradar na velocidade da pressão de prazo.
Tipos de fitness function (usaremos as duas primeiras nesta aula, as demais aparecem em outras):
- De dependência: "módulo X não pode importar o interno de Y". (foco desta aula)
- De estrutura: "todo módulo de domínio tem
core/,http/,persistence/". (foco desta aula) - De métrica: complexidade ciclomática abaixo de N, cobertura acima de M.
- De comportamento: testes de contrato, testes de camada.
O princípio comum: transforme a diretriz da wiki numa regra que quebra o build. A wiki não impede o import; o ESLint impede.
Fitness function de dependência com ESLint
Volte ao problema da aula Comunicação entre módulos: o padrão Public API Provider: queremos garantir que nenhum módulo importe o interno de outro — só a superfície pública (public/) é atravessável. Isso é expressável com no-restricted-imports usando padrões de caminho.
Configuração (ESLint flat config):
// eslint.config.js — regra: só o public/ de um módulo é importável de fora
export default [
{
files: ['src/module/**/*.ts'],
rules: {
'no-restricted-imports': ['error', {
patterns: [
{
// proíbe importar o INTERNO de qualquer módulo (core, http, persistence)
group: [
'**/module/*/core/**',
'**/module/*/http/**',
'**/module/*/persistence/**',
],
message:
'Import cruzando fronteira de módulo. Use a Public API do módulo (module/<x>/public).',
},
],
}],
},
},
];
Com isso, import { SubscriptionEntity } from '../../billing/persistence/entity' quebra o lint — e, se o lint roda no CI (e ele roda), quebra o build. O dev recebe a mensagem na hora, com a orientação certa ("use a Public API"). A regra da aula Comunicação entre módulos: o padrão Public API Provider deixou de ser convenção e virou lei aplicada por máquina.
Refinando: um módulo não importa NADA de outro, exceto o public
Uma versão mais estrita usa a granularidade de "ninguém importa o interno; só public/ pode ser importado de fora do próprio módulo". Ferramentas dedicadas (como eslint-plugin-boundaries) tornam isso declarativo:
// com eslint-plugin-boundaries
settings: {
'boundaries/elements': [
{ type: 'module-public', pattern: 'src/module/*/public/**' },
{ type: 'module-internal', pattern: 'src/module/*/(core|http|persistence)/**' },
],
},
rules: {
'boundaries/element-types': ['error', {
default: 'disallow',
rules: [
// interno pode falar com interno DO MESMO módulo (mesma distância curta, aula *As dimensões do acoplamento: força, distância e tempo*)
{ from: 'module-internal', allow: ['module-internal'], /* mesmo módulo via matcher */ },
// qualquer módulo pode consumir o PUBLIC de outro
{ from: ['module-internal', 'module-public'], allow: ['module-public'] },
],
}],
},
O detalhe importante não é a sintaxe exata (varia por versão/plugin) — é o modelo mental: você declara os "tipos" de código e quais podem depender de quais. Isso codifica a regra da distância da aula As dimensões do acoplamento: força, distância e tempo diretamente no linter.
Fitness function de estrutura
Queremos também garantir a forma de cada módulo — que ela materialize DDD + Hexagonal (aula DDD estratégico + Arquitetura Hexagonal no FakeFlix). Isto é, todo módulo de domínio deve ter core/, http/, persistence/. Ferramentas como eslint-plugin-project-structure validam isso:
// regras de estrutura de projeto
{
"rules": {
"project-structure/folder-structure": "error",
"project-structure/file-structure": "error"
},
"settings": {
"projectStructure": {
"structure": {
"src/module/{domain}/": {
"children": ["core/", "http/", "persistence/", "public/"]
}
}
}
}
}
Agora, criar um módulo payments/ sem a pasta public/, ou jogar um arquivo solto na raiz do módulo, quebra o build. A estrutura deixou de depender de o dev "lembrar do padrão" — ela é verificada. Isso protege o novo integrante do time tanto quanto o veterano cansado.
Fechando o cerco: Husky (pre-commit) + CI
Duas linhas de defesa, propositalmente redundantes:
Pre-commit (Husky) — feedback imediato, antes mesmo do push:
# .husky/pre-commit
npx lint-staged
// package.json — só linta o que mudou (rápido)
{
"lint-staged": {
"src/**/*.ts": ["eslint --max-warnings=0"]
}
}
O dev tenta commitar um import proibido → o commit é barrado localmente, com a mensagem explicativa. A violação nunca chega ao repositório. Feedback em segundos, no menor loop possível.
CI — a rede de segurança que ninguém pula:
# .github/workflows/ci.yml (trecho)
- name: Fitness functions (arquitetura)
run: npx eslint src --max-warnings=0
Por que ambos? Porque hooks locais podem ser pulados (git commit --no-verify). O CI é a autoridade final: nada entra na branch principal sem passar. O Husky otimiza o feedback; o CI garante a lei. Defesa em profundidade.
A mudança cultural: com isso, a revisão humana para de gastar energia em "você furou a fronteira aqui" (a máquina já pegou) e passa a discutir o que importa — se o design está certo, se a fronteira está no lugar certo. A automação não substitui o arquiteto; ela libera o arquiteto para pensar em vez de patrulhar.
Antes → Depois
| Antes | Depois |
|---|---|
| Fronteira documentada na wiki | Fronteira verificada no CI |
| Import furado passa no review cansado | Import furado quebra o build na hora |
| "Padrão de pastas" depende de memória | Estrutura validada por regra |
| Arquiteto patrulha PRs | Máquina patrulha; arquiteto projeta |
| Arquitetura degrada sob pressão de prazo | Arquitetura resiste à pressão |
Tradeoffs e limites (o que fitness functions NÃO fazem)
Honestidade — automação não é bala de prata:
- Pegam a letra, não o espírito. O ESLint garante que Content não importa o interno de Billing. Não garante que o design está bom: Content pode chamar a Public API de forma burra, ou a fronteira pode estar no lugar errado. A máquina defende a regra; ela não decide se a regra é a certa. Isso continua sendo trabalho humano.
- Custo de manutenção das regras. Regras muito rígidas geram atrito e falsos positivos que o time aprende a silenciar (
// eslint-disable), e aí a governança morre por mil exceções. Regras devem ser poucas, valiosas e com mensagens que ensinam. Comece pela regra que dói mais (imports cruzando fronteira) e adicione só com evidência — é a aula Por que não existe arquitetura perfeita aplicada à própria governança. - Falso senso de segurança. "Passou no lint = arquitetura ok" é perigoso. Fitness function é piso, não teto. Ela impede regressões óbvias; não substitui pensar.
- Escape hatches precisam de disciplina. Todo linter tem como desabilitar. Uma exceção legítima deve ser rara, comentada com o motivo e idealmente revisada. Exceções silenciosas viram a nova wiki ignorada.
O equilíbrio: automatize as regras objetivas e de alto valor (dependência, estrutura), e reserve o julgamento humano para o design. Fitness functions compram tempo e consistência; elas não compram bom gosto arquitetural.
Cola mental
- Fitness function = teste executável de uma propriedade arquitetural; se falha, quebra o build.
- Regra de ouro da governança: transforme a diretriz da wiki numa regra que quebra o build.
- Dependência entre módulos:
no-restricted-imports/eslint-plugin-boundariesproíbem importar o interno; sópublic/atravessa (defende a aula Comunicação entre módulos: o padrão Public API Provider). - Estrutura:
eslint-plugin-project-structuregarantecore/http/persistence/public(defende a aula DDD estratégico + Arquitetura Hexagonal no FakeFlix). - Husky = feedback rápido (pode ser pulado); CI = lei final (não pode). Use os dois.
- Limite: automação pega a letra, não o espírito. Design continua sendo trabalho humano. Poucas regras, valiosas, com mensagens que ensinam.
Referências para Aprofundamento
O conceito
- Building Evolutionary Architectures — Neal Ford, Rebecca Parsons e Patrick Kua — A origem do termo fitness function: a característica arquitetural que você não mede é a que você perde.
- Fitness Function-Driven Development — Thoughtworks — Como levar a ideia para o dia a dia do time, com exemplos de funções por característica.
As ferramentas
- ESLint — no-restricted-imports — A regra nativa que sustenta a maior parte das restrições de fronteira desta aula.
- eslint-plugin-boundaries — Permite declarar os tipos de módulo e quais podem importar quais, que é a fitness function de dependência em forma declarativa.
- dependency-cruiser — Valida e desenha o grafo de dependências; útil quando a regra é sobre o formato do grafo (ciclos, camadas) e não sobre um import específico.
- Husky e lint-staged — O gancho de pre-commit e a execução só sobre o que mudou, para a checagem não virar espera.
- ArchUnit — O equivalente no mundo Java/JVM, citado como referência de quão longe dá para levar o teste de arquitetura.
Exercícios
Exercício 1 — Escreva a regra
Você quer proibir que qualquer arquivo em src/module/content/** importe qualquer coisa de src/module/billing/ que não esteja em billing/public/. Escreva o bloco de no-restricted-imports (padrão + mensagem).
Ver resposta
✓ Resposta: javascript
'no-restricted-imports': ['error', {
patterns: [
{
group: ['**/module/billing/!(public)/**', '**/module/billing/!(public)'],
message: 'content só pode importar de billing/public. Use a Public API de Billing.',
},
],
}],
O padrão billing/!(public)/** casa qualquer subpasta de billing que não seja public (a sintaxe exata de negação pode variar por versão do ESLint/minimatch; a intenção é: bloquear tudo de billing exceto public/). A mensagem ensina o caminho certo, em vez de só negar.
Exercício 2 — Classifique a fitness function
Diga o tipo (dependência / estrutura / métrica / comportamento) de cada uma:
(a) "complexidade ciclomática máxima 15 por função";
(b) "todo módulo tem pasta public/";
(c) "content não importa billing/persistence";
(d) "existe teste de contrato para cada método da Public API".
Ver resposta
✓ Resposta: (a) Métrica — limita um número mensurável (complexidade ciclomática). (b) Estrutura — valida a forma de pastas. (c) Dependência — regra de import entre módulos. (d) Comportamento — exige que exista um teste de contrato validando o comportamento da API.
Exercício 3 — Husky vs. CI
Um dev diz: "se temos o hook de pre-commit do Husky barrando imports proibidos, o check no CI é redundante, vamos remover pra acelerar". Você concorda? Justifique com base em como hooks locais podem ser contornados.
Ver resposta
✓ Resposta: Não concordo. Hooks de pre-commit rodam na máquina do dev e podem ser pulados trivialmente com git commit --no-verify, além de dependerem de o Husky estar instalado corretamente naquele clone (nem sempre está — clone novo, ambiente de CI de outra pessoa, etc.). Ou seja, o Husky é uma conveniência de feedback rápido, não uma garantia. O check no CI é a autoridade que não pode ser contornada: nenhum PR entra na branch protegida sem passar. Remover o CI transferiria a garantia da arquitetura para a boa vontade de cada dev em não usar --no-verify — que é exatamente o tipo de "governança por esperança" que a aula inteira combate. Mantenha os dois: Husky para velocidade, CI para lei.
Exercício 4 — O limite da automação
Sua fitness function de dependência está verde: nenhum módulo importa o interno de outro. Mesmo assim, um sênior olha o código e diz "a arquitetura está degradando". Cite dois problemas arquiteturais reais que passariam por essa fitness function sem serem detectados.
Ver resposta
✓ Resposta: Dois exemplos (bastam dois):
- Fronteira no lugar errado / má modelagem. As regras garantem que ninguém fura as fronteiras existentes, mas não que as fronteiras estejam certas. Se billing e content deveriam ter um terceiro contexto entre eles (ou se um conceito foi posto no módulo errado), a fitness function fica verde enquanto o design está errado. Isso é DDD estratégico (aula DDD estratégico + Arquitetura Hexagonal no FakeFlix), julgamento humano.
- Acoplamento via contrato inchado / provider anêmico. Content pode respeitar a regra (só importa billing/public) e ainda assim a Public API pode estar vazando o modelo interno (ex.: retornando uma estrutura que espelha a entity — stamp coupling da aula As dimensões do acoplamento: força, distância e tempo). O import é "legal", mas o acoplamento semântico é forte. A máquina vê o caminho do import, não a qualidade do contrato.
- (Também válido: acoplamento temporal — Content e Billing acoplados por ordem/eventos sem import nenhum; ou baixa coesão dentro de um módulo que respeita todas as pastas.) Moral: fitness function é piso, não teto.
Exercício 5 — Desafio: a regra que dá falso verde
Um time escreveu esta fitness function para impedir que qualquer módulo importe o interior de outro:
// .eslintrc.js
rules: {
'no-restricted-imports': ['error', {
patterns: ['**/billing/core/**', '**/billing/persistence/**'],
}],
}
O CI está verde há três meses. Mesmo assim, na semana passada uma mudança de coluna em Billing quebrou Content em produção. Aponte três falhas dessa regra e diga o que fazer.
Ver resposta
✓ Resposta: (1) A regra cobre um módulo só. Ela protege o interior de billing/, e nada impede billing/ de importar content/core/**, nem identity/ de importar qualquer um dos dois. Fronteira defendida em uma direção só não é fronteira: basta a dependência nascer do outro lado para o cerco furar, e foi assim que o acoplamento entrou sem ninguém ver. A regra tem que ser simétrica e por módulo, declarando para cada um quem pode importar o quê — que é o que o eslint-plugin-boundaries faz melhor do que uma lista de padrões escrita à mão. (2) A regra também vale para o próprio módulo. no-restricted-imports com esse padrão bloqueia billing/http/ de importar billing/core/ — que é exatamente o que ele deve fazer. Na prática, ou o time desligou a regra com eslint-disable nos arquivos internos de Billing (e cada disable desses vira um buraco permanente), ou usou caminho relativo para escapar dela. Isso leva à terceira falha. (3) O padrão só casa import por caminho absoluto/alias. import { X } from '../../billing/core/service' — relativo — passa direto, porque a string não bate com **/billing/core/** em muitas configurações. A regra dá verde sobre um import que ela deveria barrar, e é o pior tipo de fitness function: a que produz confiança sem produzir garantia. O que fazer: declarar os módulos e as relações permitidas com eslint-plugin-boundaries (ou dependency-cruiser), cobrindo os três bounded contexts nos dois sentidos e normalizando caminho relativo e alias; permitir explicitamente o acesso de http/ e persistence/ ao core/ do próprio módulo; e acrescentar um teste que verifique a fitness function — um arquivo de fixture com um import proibido que o lint precisa recusar. Sem esse último passo, ninguém descobre que a regra parou de pegar; a aula insiste nisso porque uma regra silenciosamente quebrada é pior do que regra nenhuma: ela remove a vigilância humana sem colocar nada no lugar.