Primeiramente, gostaria de descrever que Pattern não é Standard:
Pattern (Algo que se repete muitas vezes ao ponto de não conseguirmos ignorar como um padrão) Standard (Algo obrigatório, que deve ser implementado sem questionamentos e de forma inevitável)
Como aluno do curso FullCycle - Curso do Wesley Willians, gostei muito das observações feitas sobre arquitetura de software pelo Uncle Bob sobre Clean Architecture e tentei implementar nesse projeto conforme aprendi no curso.
Muitos de nós construímos software da forma como a convenção nos é ensinada desde quando começamos no desenvolvimento de software. No caso do Rails, temos Rails Way e o famoso (Convention over Configuration). Ao meu ver, não há problema em iniciar um MVP sob essas condições, porém quando o sistema cresce é que os gargalos e necessidade de expansão começam a surgir de forma agressiva. Rails Way tem alguns problemas os quais irei elencar abaixo:
ORM foi um divisor de águas em uma época onde tínhamos que escrever SQL puro integrado diretamente com a interface dos muitos bancos de dados distintos. E a proposta foi fenomenal: teríamos um mapeamento do banco do lado da aplicação e essa abstração lógica nos daria maior controle sobre o banco de dados sem manipular o banco de dados diretamente.
- Porém, a alternativa foi criar modelos os quais seriam a representação fidedigna da tabela e campos do banco, trazer as validações do banco de dados, como uniq_keys, validações de length, validações de Regex e outras validações mais complexas (O céu é o limite para isso!) e por fim a regra de negócio do sistema, tudo em um arquivo só.
- Como resultado, teríamos arquivos gigantescos com parametrizações de banco, como belongs_to, many_to_many, has_one, many_to_many through: table_adjacent. Validadores e uma quantidade gigantesca de funções para modelar a regra de negócio diretamente misturada com banco de dados.
- É quase como se o banco de dados fosse o sistema em si e não só apenas um repositório de dados anêmico.
- Os controllers se tornaram inchados com mais regra de negócio além das já definidas nos modelos, e consequentemente temos controllers totalmente acoplados ao sistema, como se o controller fosse o próprio sistema em si.
- Confundimos os papéis e passamos a delegar mais função para o controller além da que ele foi projetado inicialmente que é apenas ser um portão de acesso ao sistema real (Bem, nessa etapa o que é sistema real? Se toda a modelagem está no controller...).
Passamos a ter mais formas de acessar nosso sistema além do RestAPI, via GraphQL, gRPC, WebSocket, Socket TCP puro, SOAP, outras integrações com APIs de terceiros. Além das formas de acessar a regra de negócio via eventos, usando RabbitMQ ou Apache Kafka.
Agora temos diversas formas de armazenar informação além do banco de dados SQL normal, como NoSQL (MongoDB, ElasticSearch), Banco de dados em memória (Redis), Banco de dados baseado em grafos como (Neo4J).
Imagina se quiséssemos dividir nosso sistema em microserviços? Usando K8s (Meu deus, o que é microserviço! Só sei fazer código em controller e modelo!). A verdade é que sistemas acoplados ao Framework estão fadados a se tornarem quase impossíveis de serem expandidos para melhores abordagens.
Ao fazer o curso com Wesley Willians, percebi que poderia desacoplar meu sistema do mundo exterior e então emergi no DDD que, ao entender, me pareceu ser uma abstração mais sofisticada do UML. Bacana, agora tinha uma forma mais elegante de projetar software, mas no módulo subsequente conheci o Clean Architecture.
O DDD já criticava bastante a dependência do ORM e já comentava formas de desacoplar a regra de negócio do Framework e dos softwares de persistência de dados.
O que você vai ver aqui é uma abstração completa do Framework e do ORM. Ou seja, temos agora uma regra de negócio que subsiste por si só sem depender de nada no mundo externo.
Uncle Bob fala que o core do sistema deve ser modelado em Entity (Entidades), e que as validações da regra de negócio devem ser feitas com UseCases. Caso queiramos nos conectar com o mundo externo, teremos DTOs (Data transfer objects), Presenters. E para acesso a dados teremos Adapters.
Na raiz do projeto foi criada a pasta core_business_rules.
- Inicialmente iremos olhar para a pasta
entity, onde modelamos classes básicas como OOP orienta que é a entidade básica do circle e do frame. - Seguindo a sequência lógica, olharemos agora para a pasta
usecase, onde teremos a lógica negocial, ou seja, usamos as classes base para representar nosso objeto mais básico, porém o usecase fica responsável por modelar o processo, ou os motivos pelos quais as coisas acontecem em uma empresa em forma de software. - Dentro da pasta
usecasetemos osdto, que são apenas interfaces (contratos), que parametrizam a entrada de dados para os usecases. Ou seja, o use case agora depende de interface, e quem implementar essa interface pode ser injetada na nossa regra de negócio, ou seja, obrigamos uma formatação para que o mundo externo converse com nossa regra de negócio. - Na pasta
output, temos os presenters, o que essas classes irão fazer é modelar interfaces para exportar dados para os diferentes mecanismos que desejarem se comunicar com nossa regra de negócio (RestAPI, GraphQL, gRPC, WebSocket, Socket TCP puro, SOAP, outras integrações com APIs), ou seja, cada uma dessas interfaces tem sua própria forma de estabelecer contrato de comunicação. Assim cada contrato fica separado em arquivos distintos. - A pasta
repository, temos os adapters, aqui colocamos o ORM (Active Record) como uma interface a ser injetada na nossa regra de negócio. Uma vez que deixamos o ORM como uma dependência totalmente desacoplada, desvinculamos completamente nossa regra de negócio dos mecanismos de persistência de dados SQL normal, como NoSQL (MongoDB, ElasticSearch), Banco de dados em memória (Redis), Banco de dados baseado em grafos como (Neo4J). - Por fim criamos
core_business_rules.rb, usamos o Pattern GoF Factory e Facade para unificar o acesso à nossa regra de negócio por um provider único no nosso sistema.
Se olhar os controllers irá perceber que, agora o controller lida apenas com lógica de controller. E que para conseguir interagir com nossa regra de negócio tem que estabelecer um contrato via DTO para transformar os parâmetros em um formato que nossa regra de negócio aceite, ou seja, qualquer input (RestAPI, GraphQL, gRPC, WebSocket, Socket TCP puro, SOAP, outras integrações com APIs) que quiser interagir com nossa regra de negócio terá que usar o DTO como contrato de acesso, caso contrário simplesmente não conversará com nossa regra de negócio.
O retorno foi empacotado nos outputs, ou seja, cada output faz o contrato de presenter para os mecanismos que desejam interagir com nossa regra de negócio. No caso, temos um output para construir retornos em JSON para nosso controller. O principal aqui sobre os outputs é que a lógica do presenter não está mais no controller, apenas o output conhece a lógica de transformação de dados.
O adapter da pasta repository é gerenciado por nossa regra de negócio, a qual injeta o ORM para manipularmos os dados, mas como a dependência depende da interface, posso injetar qualquer adapter SQL normal, como NoSQL (MongoDB, ElasticSearch), Banco de dados em memória (Redis), Banco de dados baseado em grafos como (Neo4J), para manipularmos e persistirmos dados.
Agora, como temos nossa regra negocial separada em entities e usecases, podemos facilmente criar novos serviços apenas copiando e colando a regra de negócio específica em outro app Rails, Sinatra, Django, Phoenix, Go-lang. Ou seja, como é classe e OOP, fica muito fácil migrar entre diferentes tecnologias.
Além de ficar muito fácil integrar com modelos voltados a eventos como CQRS e Event Driven para escalarmos com RabbitMQ e/ou Apache Kafka.
External World → Controller → DTO → UseCase → Entity
↑ ↓
Output ← Presenter ← Repository (Adapter)
- Controller recebe requisição e cria DTO
- UseCase processa usando entities e repository
- Repository abstrai persistência
- Output Adapter formata resposta
- Controller retorna resposta formatada
Ao analisar os testes, fica evidente que a Clean Architecture torna o projeto mais conceitual e orientado a classes, eliminando grande parte da complexidade normalmente associada aos testes que dependem de banco de dados. Como os modelos e controllers são mantidos enxutos, é possível testar a lógica de negócio de forma isolada, sem necessidade de mockar dados persistidos, tornando os testes mais simples, rápidos e confiáveis.
# 1. Clone o repositório
git clone <repository-url>
cd squares_and_circle_api_v2
# 3. Ou iniciar manualmente com Docker
docker-compose up -d
# 3. Preparar banco de dados
docker-compose exec web rails db:prepare
# 4. Carregar dados de exemplo
docker-compose exec web rails db:seed
# Testes com Docker
docker-compose exec web bundle exec rspec
# 4. Acessar a aplicação
# 🌐 API: http://localhost:3000
# 📖 Swagger: http://localhost:3000/api-docs
# 🧪 Testes especificos: bundle exec rspec spec/requests/integration_spec.rb:54 --format documentation
# 🧪 Testes gerais: bundle exec rspec --format documentationNeste projeto usamos o SonarQube para nos ajudar a verificar a cobertura dos testes e receber insights sobre nosso código e possíveis refatorações.
# Se rodou tudo com docker-compose você conseguirá executar o coverage
# 1. Configurar projeto (primeiro acesso: admin/admin)
# Acesse: http://localhost:9000
# 2. Executar análise com coverage
./run_coverage.sh
# 3. Executar sonarqube completo
./run_sonarqube.sh
# 3. Enviar para SonarQube
sonar-scanner \
-Dsonar.projectKey=squares-circles-api \
-Dsonar.sources=. \
-Dsonar.host.url=http://localhost:9000 \
-Dsonar.login=YOUR_TOKEN \
-Dsonar.ruby.coverage.reportPaths=coverage/lcov/squares_and_circle_api_v2.lcovTalvez seja nescessário configurar o ambiente virtual do python3 .venv para o SonarQube funcionar corretamente:
python -m venv .venv
source .venv/bin/activate