Do SOLID à Arquitetura Limpa 📐🧱🧹

Fala pessoal! Beleza?

Nesse post quero revisar os principais conceitos abordados no livro “Arquitetura Limpa” do Robert C. Martin. A ideia é fazer um resumão para entendermos como usamos os princípios SOLID para chegar até a Arquitetura Limpa. Além da indicação do próprio livro, vou deixar no final do post vários link de conteúdos relacionados ao tema e que me ajudaram a entender melhor esse assunto. Bora lá que o post vai ser grande!

PS 1: Os exemplos de código foram pensado utilizando a linguagem Java! PS 2: Esse conteúdo estava originalmente no meu Github para um vídeo que fiz no Youtube, mas fiz algumas alterações para postar aqui.

SOLID

A Arquitetura Limpa surge da aplicação do SOLID como ferramenta para se construir uma software capaz de crescer de forma saudável. Não temos um modelo fixo de como devemos implementar uma arquitetura, mas técnicas para utilizarmos de acordo com as necessidades do projeto.

Princípio da inversão de dependência (DIP)

O Princípio da Inversão de Dependência diz que “os sistemas mais flexíveis são aqueles em que as dependências de código-fonte se referem apenas a abstrações e não a itens concretos”. Mas calma, implementações concretas como a classe String, por exemplo, não são um problema, já que elas dificilmente vão mudar. Também não tem problema depender de classes concretas quando estas pertencem à mesma camada de quem está usando elas: uma classe Controller que depende de uma classe Request, por exemplo.

O que realmente queremos proteger são códigos de níveis mais altos, mais próximos à regra de negócio (o que chamaremos de Entidades), de códigos de níveis mais baixos, mais próximos à infraestrutura. Não queremos uma classe Service dependendo de uma classe DAO diretamente!

Para aplicar a inversão de dependência, cria-se uma abstração para definir um contrato baseando-se nas necessidades do negócio, que deverá ser implementada por uma camada de nível mais baixo. Por exemplo: se há a necessidade de registrar um novo User, criaremos uma abstração para isso (uma interface) e como ela será implementada não importa: pode ser implementada para realizar a persistência em um banco de dados, ou enviar para um serviço via http ou enviar via um broker de mensageria. Para o código de negócio, isso não fará diferença, contanto que o contrato seja respeitado.

Iversão de pedendência

Princípio da responsabilidade única (SRP)

O Princípio da Responsabilidade Única diz que “um módulo só deve ter uma, e apenas uma, razão para mudar”. Mas essa definição, segundo o livro, na verdade cabe apenas às funções/métodos.

Um software existe para atender as demandas de usuários ou stakeholders, que podem fazer parte de grupos conhecidos pela definição do negócio (ex: usuário comum ou admin, CTO ou CEO). Cada um desses grupos exige que esse software mude por motivos diferentes e, portanto, pode ser referido como um Ator. Ou seja, o código fonte do sistema deve ser separado e agrupado baseando-se em cada ator que o utiliza, pois esse código irá mudar por motivos diferentes. Dessa forma, a definição correta do SRP é:

“Um módulo deve ser responsável por um, e apenas um, ator”.

Princípio da segregação de interfaces (ISP)

O Princípio de Segregação de Interfaces busca orientar o desenvolvimento de forma que um código não dependa de coisas que não irá utilizar, poupando assim, uma compilação e implantação desnecessárias. Porém, além desses detalhes de baixo nível por trás do ISP, podemos utilizá-lo em níveis arquiteturais junto com SRP para criar artefatos mais coesos, reutilizáveis e independentes. Por exemplo: imagine um micro serviço que depende de um monolito, e uma nova versão desse monolito esteja sendo entregue em PROD, alterando uma feature completamente diferente da utilizada pelo micro serviço. Agora imagine que houve uma falha nessa entrega e que causou um downtime no monolito, impactando esse micro serviço com uma alteração que não tem nada a ver com a feature que ele utiliza.

Princípio da substituição de Liskov (LSP)

O Princípio da Substituição de Liskov diz que “Se, para cada objeto o1 do tipo S existir um objeto o2 do tipo T, de modo que todo programa P definido em termos de T, o comportamento de P é inalterado quando o1 é substituído por o2, então S é um subtipo de T.”

Esse princípio é sobre heranças e implementações, é um guia para garantir que os subtipos possam ser substituídos com segurança, sem alterar o comportamento do software ou erros inesperados. E para garantir isso, é necessário conhecer como ele deve funcionar, conhecer sobre o negócio, sobre os processos, casos de uso, para se realizar code review e criar testes efetivos. Não podemos realizar uma implementação de qualquer jeito, se o contrato de uma interface define uma busca de dados, não podemos realizar uma persistência em sua implementação, por exemplo.

Exemplo: O problema do quadrado/retângulo

Diagrama do quadrado/retângulo

Um quadrado (Square) não é uma extensão válida de um retângulo (Rectangle). Isso porque a área dos quadrados é definida em função de um único valor: o tamanho de qualquer um de seus lados. Em contrapartida, um retângulo tem sua área definida em função de dois valores: largura e altura. Portanto, dado o diagrama acima, o código a seguir não funcionaria, caso a instância passada para a variável “r” fosse um Square:

Código do quadrado/retângulo

Neste exemplo, poderia ser implementado um IF para verificar se a instância de “r” é um retângulo para então realizar o assert. Mas a necessidade de criar esse IF para que o código não quebre, demonstra a violação do LSP e também do DIP.

Princípio do aberto fechado (OCP)

O Princípio Aberto Fechado diz que “Um artefato de software deve ser aberto para extensão, mas fechado para modificação”. “Uma boa arquitetura de software deve reduzir a quantidade de código a ser mudado para o mínimo possível. Zero seria o ideal”. Novas funcionalidades deveriam entrar como novos códigos, sem alterar os já existentes. Códigos existentes só deveriam ser alterados caso as regras de negócios que eles implementam se alterem.

Este é o princípio mais dependente dos outros princípios, quase uma consequência. Principalmente do SRP e do DIP, que, quando aplicados, separam as responsabilidades em códigos coesos e protege códigos de níveis mais altos dos de níveis mais baixos, respectivamente. Dessa forma, os códigos de níveis mais altos se tornam mais fechados à mudanças e abertos a extensões.

ARQUITETURA LIMPA

Arquitetura Limpa

A arquitetura limpa, criada por Robert Martin, é baseada na arquitetura hexagonal, na arquitetura DCI (Data, Context and Interaction), BCE (Entity, Control and Boundary) e tem sua base fundada nos princípios SOLID e na Regra da Dependência.

A Regra da dependência

“As dependências de código-fonte devem apontar apenas para dentro, na direção das políticas de nível mais alto”

Logo, a parte mais externa do diagrama circular (códigos de níveis mais baixos) é mais volátil e concreta; em contrapartida, a parte mais interna (códigos de níveis mais altos) é a mais estável e abstrata. Essa regra é uma reafirmação ao Princípio do Aberto Fechado. Note como as setas sempre apontam para dentro, nunca para fora.

Entidades (Regras de negócio da empresa)

Entidade é todo código que reúne as Regras Cruciais de Negócio da empresa, as políticas de alto nível. Não importa se é uma classe, uma estrutura de dados, interfaces, etc. Elas também são, geralmente, as mais abstratas e abertas à extensão. Portanto, podem ser reutilizáveis em aplicações diferentes da empresa.

Caso de uso (Regras de negócio da aplicação)

Um caso de uso é a descrição da maneira de como um sistema automatizado é usado. Ele especifica a entrada a ser fornecida pelo Ator, a saída a ser retornada para esse Ator e os passos para o processamento das entidades envolvidas na produção dessa saída. Ou seja, o caso de uso orquestra as Entidades para atender, de forma automatizada, as necessidades de um Ator. Cada caso de uso representa uma funcionalidade que um Ator irá utilizar, seguindo o Princípio da Responsabilidade Única.

Adaptadores de Interface

Os códigos dessa camada são responsáveis por converter os dados vindos de algum agente externo no formato mais conveniente para os casos de uso. Também são responsáveis por converter os dados de saída de um caso de uso, para um formato mais conveniente para o agente externo. Aqui é onde brilha o uso do Princípio da Inversão de Dependência.

Frameworks e Drivers

Nessa camada é realizada as conexões com frameworks e drivers. É onde ficam todos os detalhes: web, base de dados, serviços externos, etc. Essa é a camada mais concreta e volátil.

Um cenário típico

Esse diagrama é baseado nos diagramas apresentados no livro nas páginas 208 e 72 para representar a implementação mais fiel da Arquitetura Limpa - na prática, ninguém usa ela ao pé da letra.

Um cenário típico

Primeiro, a View realiza uma Request para um Controller, que monta o RequestModel e passa o mesmo para o UseCase, através da sua interface de entrada InputPort, que orquestra as Entities. Ao final, realiza uma chamada ao OutputPort, cujo a implementação é o Presenter, responsável por montar o ResponseModel e repassá-lo para a View.

Note que a implementação de SomeGateway, que é uma Entity, está na camada de Interface Adapters. Nesse ponto foi realizada uma inversão de dependência para não quebrar a regra da dependência. Ao mesmo tempo, ganha-se extensibilidade, visto que pode haver mais de uma implementação para SomeGatway. Assim fica fácil trocar a busca de um dado em um banco de dados por uma busca a um outro serviço web, por exemplo. Portanto, todas as dependências sempre apontam para uma camada de nível maior.

Como cada caso de uso é responsável por um ator, logo, toda essa estrutura é feita para a implementação de uma funcionalidade que esse ator poderá utilizar.

Conclusão

A Arquitetura Limpa é um modelo de arquitetura, que surge naturalmente da aplicação dos princípios SOLID e da regra da dependência. Seu objetivo é propor um modelo de referência de como criar aplicações independentes de frameworks, UI, banco de dados e qualquer agente externo. Por ser independente, se torna fácil e barata de se testar. Garante extensibilidade e protege os códigos de alto nível, tornando-as mais estáveis.

Por outro lado, implementá-la ao pé da letra pode ser muito complexo e ineficiente. Por isso é comum encontrar versões simplificadas desse modelo de arquitetura. Por isso, trarei em um post futuro como podemos simplificar a Arquitetura Limpa para que possamos criar aplicações bem estruturadas para proteger nossos códigos de negócio, sem precisar abrir mão das facilidades que os frameworks trazem para nós.

REFERÊNCIAS

Robert C. Martin - Arquitetura Limpa (Livro)

Full Cycle - Clean Architecture

Como DEV ser! - Entenda CLEAN ARCHITECTURE de uma vez por todas! 🧻 | Como DEV ser!

Otavio Lemos - 45 - Clean Architecture

Full Cycle - Arquitetura Hexagonal: O que você precisa saber

Spring I/O - Clean Architecture with Spring by Tom Hombergs @ Spring I/O 2019

UnityCoin - Clean Code - Uncle Bob / Lesson 1

UnityCoin - Clean Code - Uncle Bob / Lesson 2

UnityCoin - Clean Code - Uncle Bob / Lesson 3

UnityCoin - Clean Code - Uncle Bob / Lesson 4

UnityCoin - Clean Code - Uncle Bob / Lesson 5

UnityCoin - Clean Code - Uncle Bob / Lesson 6