A abordagem de design em camadas do Go
(jerf.org)- A linguagem Go proíbe estritamente referências circulares entre pacotes, o que naturalmente induz um design em camadas (layered design)
- Este texto explica a estrutura em camadas que projetos Go acabam tendo obrigatoriamente e defende que ela já é suficientemente válida sem precisar impor outra arquitetura por cima
- Quando surge uma dependência circular, o texto apresenta estratégias de refatoração concretas e práticas em etapas para resolvê-la
- Cada pacote é projetado para ter uma unidade funcional com significado próprio, o que favorece testes, manutenção e separação em microsserviços
- No fim, essa abordagem evita o problema comum no design de código de "você queria uma banana, mas acabou trazendo a selva inteira"
Abordagem de design em camadas no Go
Princípios básicos
- O Go proíbe referências circulares entre pacotes
- As relações de import de todo programa Go devem formar um grafo acíclico direcionado (DAG)
- Essa estrutura não é uma escolha, mas uma regra de design imposta no nível da linguagem
Formação automática do empilhamento de pacotes
- Excluindo os pacotes externos, os pacotes internos do projeto podem ser automaticamente organizados em camadas de acordo com a profundidade das referências
- Como na figura abaixo, na base ficam pacotes utilitários centrais, como metrics, logging e estruturas de dados compartilhadas
- Depois, os pacotes superiores vão combinando funcionalidades e se empilhando gradualmente para cima
Características dessa forma de projetar
- As camadas se baseiam na direção das referências, não em abstrações hierárquicas
- Um pacote pode referenciar vários pacotes de nível inferior
- Abordagens clássicas como MVC e arquitetura hexagonal também podem ser aplicadas sobre essa estrutura
→ Mas é indispensável considerar as restrições estruturais do Go
Estratégias para resolver referências circulares
Quando surgir uma referência circular, tente a refatoração na ordem abaixo:
1. Mover funcionalidade
- A abordagem mais recomendada
- Analise com precisão a funcionalidade que causa o ciclo e mova-a para o lugar logicamente adequado
- Não é usada com tanta frequência, mas é a que mais melhora a clareza conceitual
2. Separar a funcionalidade compartilhada em outro pacote
- Mova tipos ou funções usados em comum pelos dois lados (
Username, por exemplo) para um terceiro pacote - Mesmo que o pacote pareça pequeno, separe sem medo
→ Com o tempo, há grande chance de esse pacote crescer
3. Criar um pacote superior de composição
- Crie um terceiro pacote que componha os dois pacotes em ciclo
- Ex.: separar a dependência bidirecional entre
CategoryeBlogPostem um pacote superior
→ Os pacotes inferiores continuam como structs simples, e a funcionalidade real é composta no pacote superior
4. Introduzir interfaces
- Substitua a dependência por uma interface que tenha apenas os métodos necessários para a struct ou função
- Remove dependências desnecessárias e melhora a facilidade de teste
- Mas, se usado em excesso, isso pode acabar deixando o design mais complexo
5. Copiar
- Se o alvo da dependência for muito pequeno, simplesmente copie e use
- Pode parecer uma violação de DRY, mas em muitos casos isso ajuda a deixar o design mais claro
6. Unir em um único pacote
- Se nenhum dos métodos acima for possível, mescle os dois pacotes
- Se não virar um pacote grande demais, isso pode ser aceitável
→ Ainda assim, evite fazer essa fusão de forma automática e decida com cuidado
Vantagens práticas dessa abordagem
- Cada pacote tem uma unidade funcional significativa por si só e pode ser testado de forma independente
- Como as referências dentro do pacote são limitadas, é possível entender um pacote isoladamente sem precisar compreender todo o código
- Evita ligações globais de dependência não intencionais (= o problema da selva) e incentiva a escrever código que usa apenas o necessário
- Também é fácil extrair para microsserviços quando necessário
→ A maioria das dependências já está claramente definida
Conclusão
- As restrições de design de pacotes no Go não são um incômodo, mas um mecanismo que induz bom design
- Mesmo sem uma arquitetura especial, é possível implementar um design robusto apenas com a estrutura de referências entre pacotes
- A análise cuidadosa e as estratégias de refatoração para referências circulares são úteis não só no Go, mas também em outras linguagens
4 comentários
No começo, quando você monta tudo correndo e faz funcionar, é divertido.
Mas, quando começa a colocar testes,
você acaba pensando por que fez daquele jeito naquela época.
A frase "eu queria uma banana, mas trouxeram a floresta inteira" é muito engraçada.
Acho que uma das coisas mais difíceis ao desenvolver com Spring era a dependência circular..
Aquela frustração de ficar inicializando um ao outro infinitamente e acabar travando por vazamento de memória...
Comentários no Hacker News
Não permitir dependências circulares é uma excelente escolha de projeto ao construir programas de grande porte
Excelente post de blog
Uma técnica bônus relacionada ao conselho de "mover para um terceiro pacote"
Parece que está lendo um livro sobre o método estruturado de Yourdon
Pacotes não podem fazer referência circular entre si
go:linknameLembra o conceito concreto de randomizer
Uma característica curiosa de Golang é que você não pode ter dependências circulares no nível de pacote, mas pode tê-las no
go.modUma ótima explicação de como Jerf pensa sobre pacotes e lida com dependências circulares