Definindo limites de módulos no Nx com Module Boundaries
O desenvolvimento de software moderno frequentemente envolve projetos complexos, equipes multidisciplinares e uma variedade de tecnologias. Nesse cenário, a abordagem tradicional de separação de código de forma monolítica muitas vezes se mostra inadequada para manter a escalabilidade, a manutenibilidade e a agilidade do projeto.
Quando um sistema possui diversos escopos e diversas camadas de arquitetura, fica difícil de delimitar os relacionamentos entre essas camadas, principalmente para novos desenvolvedores que acabaram de entrar no projeto. Nesses casos o caos se instaura e temos muitos problemas de escala.
É aqui que entram os “Module Boundaries” (Limites de Módulos), uma prática de design que desempenha um papel crucial na arquitetura e organização do código. No contexto do Nx Build System, a adoção de Module Boundaries se torna ainda mais relevante, permitindo um desenvolvimento mais estruturado e colaborativo, além de deixar bem claro o que um módulo pode importar ou não.
O que é o Nx Build System?
O Nx é um conjunto de ferramentas de desenvolvimento projetado para auxiliar no desenvolvimento de aplicações em escala, permitindo a criação de monorepos eficientes que abrigam diversos projetos relacionados. Com o Nx, é possível compartilhar código, estabelecer dependências claras entre projetos e executar tarefas como build, testes e linting de maneira otimizada.
Se você nunca utilizou o Nx antes, então não deixe de conferir meus artigos sobre Como eu inicio projetos Angular e Como eu inicio projetos React. Eu garanto que você também vai começar a criar seus projetos com ele. 😉
O que são Module Boundaries?
Module Boundaries se referem à prática de definir limites claros e bem definidos entre diferentes partes de um projeto. Em vez de uma abordagem monolítica ou hierárquica, em que todas as funcionalidades e módulos estão interconectados, os Module Boundaries promovem a criação de fronteiras entre módulos com propósitos específicos. Esses limites delimitam onde determinada funcionalidade ou conjunto de funcionalidades pertence e estabelecem interfaces claras de comunicação.
E tudo isso é definido pelo ESLint com a análise estática do código. Portanto se um módulo tentar importar indevidamente um outro módulo, seremos avisados imediatamente pela nossa IDE e também ao rodar o lint do projeto.
Projeto Nx modularizado
Utilizaremos como exemplo o projeto do artigo Como eu inicio projetos Angular. Este é o repositório do projeto. Nele estamos utilizando o Nx e separando a aplicação em diversos módulos. Nossas áreas de domínio incluem produtos, pedidos e alguns componentes UI (User Interface) mais genéricos.
Rodando o comando npx nx graph
podemos visualizar o desenho de suas dependências:
A lib modules-shared-ui
é importada pelas nossas duas libs de negócio: modules-order
e modules-products
. E a nossa app representada pelo my-app
importa nossas libs de negócio.
Precisamos impor alguns limites para esses módulos:
- Queremos permitir que
modules-orders
importemodules-shared-ui
mas não o contrário; - Queremos permitir que
modules-products
importemodules-shared-ui
mas não o contrário; - Queremos permitir que
modules-orders
importemodules-products
mas não o contrário; - Queremos permitir que todas as libs importem
modules-shared-ui
mas não o contrário.
Definindo os limites
Ao construir essas barreiras, é importante nomearmos nossos escopos. Você pode nomear da forma que se adeque melhor ao seu projeto. Porém o Nx sugere dois tipos de dimensões:
- type: Qual o tipo da sua lib. Exemplos:
feature
,data-access
,ui
,utils
(veja mais em Library types) - scope (domínio): qual área de domínio essa lib representa. No exemplo do nosso projeto podemos ter:
orders
,products
,shared
.
Agora que já temos as definições dos tipos e escopos de nossas libs, vamos criar tags para elas. Cada lib no Nx possui um arquivo project.json
onde podemos definir suas tags.
// modules/orders/project.json
{
...
"tags": ["type:feature", "scope:orders"],
...
}
// modules/products/project.json
{
...
"tags": ["type:feature", "scope:products"],
...
}
// modules/shared/ui/project.json
{
...
"tags": ["type:ui", "scope:shared"],
...
}
A seguir, vamos criar um conjunto de regras baseadas nestas tags:
type:feature
deve ser capaz de importar detype:feature
etype:ui
type:ui
só deve ser capaz de poder importar detype:ui
scope:orders
deve ser capaz de importar descope:orders
,scope:shared
escope:products
scope:products
deve ser capaz de importar descope:products
escope:shared
Para cumprir as regras, o Nx disponibiliza uma regra do ESLint personalizada. Vamos abrir o arquivo .eslintrc.base.json
na raiz do workspace e adicionar o array depConstraints
em @nx/enforce-module-boundaries
.
{
...
"overrides": [
{
...
"rules": {
"@nx/enforce-module-boundaries": [
"error",
{
"enforceBuildableLibDependency": true,
"allow": [],
"depConstraints": [
{
"sourceTag": "*",
"onlyDependOnLibsWithTags": ["*"]
},
{
"sourceTag": "type:feature",
"onlyDependOnLibsWithTags": ["type:feature", "type:ui"]
},
{
"sourceTag": "type:ui",
"onlyDependOnLibsWithTags": ["type:ui"]
},
{
"sourceTag": "scope:orders",
"onlyDependOnLibsWithTags": [
"scope:orders",
"scope:products",
"scope:shared"
]
},
{
"sourceTag": "scope:products",
"onlyDependOnLibsWithTags": ["scope:products", "scope:shared"]
},
{
"sourceTag": "scope:shared",
"onlyDependOnLibsWithTags": ["scope:shared"]
}
]
}
]
}
},
...
]
}
Testando as barreiras
Agora que já realizamos nossas configurações, vamos testar se essas restrições estão funcionando.
A partir das nossas regras, sabemos que scope:products
não deve depender de scope:orders
. Então para testar, vamos importar OrdersModule
dentro de ProductsModule
.
Imediatamente visualizamos um erro pelo Plugin do ESLint: um projeto tagueado com scope:products
somente pode depender de libs tagueadas com scope:products
ou scope:shared
. Isso é muito poderoso!
Além disso, se rodarmos o lint do nosso projeto, teremos o seguinte erro:
Portanto, se integrarmos o lint dentro do seu Continuous Integration (CI) garantimos que nada saia do controle.
Conclusão
A utilização de Module Boundaries é uma prática essencial para projetos de software bem-sucedidos e escaláveis. No contexto do Nx Build System, a definição clara de limites de módulos facilita a criação, manutenção e colaboração em projetos complexos.
Com Module Boundaries, você está construindo uma arquitetura que promove escalabilidade, manutenibilidade e colaboração eficaz, criando um ambiente propício para o desenvolvimento ágil e eficiente.