Clean Architecture com Quarkus e ArchUnit

Guilherme Faleiros
mobicareofficial
Published in
8 min readJan 4, 2021

Introdução

Neste artigo demonstrarei de maneira prática, como construir uma aplicação com o framework Quarkus utilizando a abordagem de Clean Architecture e testando a integridade da arquitetura com a biblioteca ArchUnit.

O que é o Clean Architecture?

Na busca por desenvolver aplicações rápidas, escaláveis e de fácil manutenibilidade, diversos estilos arquiteturais surgiram para atender os mais diversos tipos de software. Robert C. Martin, o “Uncle Bob”, uma das grandes lendas da Engenharia de Software com grandes clássicos como Clean Code e Clean Coder, propôs, com base em toda sua experiência adquirida ao longo da carreira, um estilo arquitetural que deu nome a uma de suas mais recentes obras: Clean Architecture: A Craftsman’s Guide to Software Structure and Design.

No livro, Robert Martin inicia com uma breve introdução à Arquitetura de Software, e algumas terminologias importantes para compreensão do assunto. Logo após, o autor descreve inúmeros casos práticos e constrói uma fundamentação para justificar a utilização deste estilo arquitetural. Os pilares do Clean Architecture são: testabilidade e desacoplamento de qualquer agente externo. Para construir a ideia de Clean Architecture, o autor se apoia em conceitos como: Domain-Driven Design, Test-Driven Development, SOLID e Design Patterns.

Enterprise Business Rules: aqui se encontram as entidades ligadas à aplicação.

Application Business Rules: nesta camada temos as regras de negócio específicas, em outras palavras, trata-se do contrato das funcionalidades que a aplicação fornecerá.

Interface Adapters: aqui se trata de uma camada que tem por função realizar uma comunicação com frameworks e bibliotecas externas de forma que o acoplamento destes com a camada de negócio seja o mais baixo possível, portanto, esses adapters obedecem um contrato específico com a camada de regras de negócio e fica a cargo do adapter fazer a implementação específica para cada caso.

Frameworks & Drivers: Bibliotecas, frameworks, interface de usuário, etc. Aqui se encaixa qualquer coisa que seja externa à aplicação.

Implementação do Clean Architecture em uma aplicação Java com Quarkus

Requisitos:

  • Java 11
  • Maven 3.6.2+
  • Docker 18.03
  • Docker Compose 3.7

Para este projeto, utilizaremos o framework Quarkus, o framework Java cloud native supersônico subatômico que vem crescendo bastante em popularidade e tendo bastante aderência em alguns projetos aqui na Mobicare. Para criar um projeto Quarkus, uma das alternativas é acessar o site: https://code.quarkus.io/ e gerar um projeto com as dependências que desejar.

Para este projeto, utilizaremos Maven, aqui se encontra as dependências que utilizaremos (pom.xml):

<dependencies><dependency><groupId>io.quarkus</groupId><artifactId>quarkus-arc</artifactId></dependency><dependency><groupId>io.quarkus</groupId><artifactId>quarkus-resteasy</artifactId></dependency><dependency><groupId>io.quarkus</groupId><artifactId>quarkus-junit5</artifactId><scope>test</scope></dependency><dependency><groupId>io.rest-assured</groupId><artifactId>rest-assured</artifactId><scope>test</scope></dependency><dependency><groupId>io.quarkus</groupId><artifactId>quarkus-resteasy-jsonb</artifactId></dependency><dependency><groupId>io.quarkus</groupId><artifactId>quarkus-hibernate-orm-panache</artifactId></dependency><dependency><groupId>io.quarkus</groupId><artifactId>quarkus-jdbc-mysql</artifactId></dependency><dependency><groupId>org.jodd</groupId><artifactId>jodd-core</artifactId><version>5.0.13</version></dependency><dependency><groupId>org.eclipse.microprofile.openapi</groupId><artifactId>microprofile-openapi-api</artifactId><version>1.1.2</version></dependency><dependency><groupId>com.tngtech.archunit</groupId><artifactId>archunit-junit5-engine</artifactId><version>0.14.1</version><scope>test</scope></dependency></dependencies>

Para esta aplicação, utilizaremos um banco de dados MySQL dentro de um container docker, iremos especificar e subir este container através de um arquivo docker-compose.yml, que deverá estar na raiz do projeto:

version: “3.7”services:mysql-clean-quarkus:image: mysql:8.0container_name: mysql-clean-quarkusenvironment:- MYSQL_DATABASE=quarkus- MYSQL_ROOT_PASSWORD=quarkus- MYSQL_USER=quarkus- MYSQL_PASSWORD=quarkusports:- “3306:3306”

Após criado, basta rodar o comando docker-compose up -d na pasta raiz do projeto para criar e subir o container.

Para conectar nossa aplicação com a nossa base de dados, basta fornecer as credenciais no arquivo application.properties do projeto:

quarkus.datasource.jdbc.url=jdbc:mysql://localhost:3306/quarkusquarkus.datasource.db-kind=mysqlquarkus.datasource.username=quarkusquarkus.datasource.password=quarkus

Primeiramente, precisamos entender do que se trata nosso projeto: Faremos uma API RESTful, onde será possível cadastrar um funcionário em uma base de dados MySQL. Agora, veja como será a divisão de nosso packages:

  • Domain: aqui definiremos nossos contratos de casos de uso e definiremos as entidades do sistema.
  • Data: aqui irão conter as implementações dos contratos de casos de uso do sistema.
  • Infra: aqui teremos implementações de adapters para frameworks e bibliotecas externas, faremos a integração da aplicação com o Hibernate Panache ORM e com o BCrypt.
  • Presentation: esta será a porta de entrada da nossa aplicação, em que iremos disponibilizar nossos recursos em endpoints HTTP.
  • Main: aqui uniremos todos os componentes fornecidos pelas camadas de Infra e Data fornecendo uma instância denominada Service para a camada de Presentation realizar a operação detalhada pelo caso de uso em questão. No nosso caso, criar um usuário. Notem que esta camada é onde o grau de acoplamento é mais alto, e isso é proposital.

Agora, vamos checar como ficou a implementação de nossa demonstração. Começando pela base de tudo, a nossa camada de Domain:

>> Implementação da camada Domain:

Nossa classe de entidade (da regra de negócio) Employee.java:

public class Employee {public Long id;public String name;public String email;public String password;}

Nossa interface que descreve o caso de uso de criação de um usuário CreateEmployee.java:

public interface CreateEmployee {Employee create(CreateEmployeeDTO employee);}

Também temos uma classe auxiliar DTO (Data Transfer Object) CreateEmployeeDTO.class:

public class CreateEmployeeDTO {public String name;public String email;public String password;}

>> Implementação da camada Data:

Nossa interface que descreve o funcionamento de métodos que farão comunicação com alguma base de dados (neste caso, relacional). EmployeeRepository.java:

public interface EmployeeRepository {Employee findByEmail(String email);Employee create(CreateEmployeeDTO employee);}

O mesmo faremos para a nossa função de criptografia, na qual usaremos para gerar um hash da senha do nosso funcionário, visto que não é boa prática armazenar a senhas de maneira clara em um banco de dados. Encrypter.java:

public interface Encrypter {String hash(String plain);}

Agora, definidas essas interfaces, criaremos a implementação concreta do nosso caso de uso CreateEmployee definido na camada de Domain:

public class CreateEmployeeImpl implements CreateEmployee {private EmployeeRepository employeeRepository;private Encrypter encrypter;public CreateEmployeeImpl(EmployeeRepository employeeRepository,Encrypter encrypter) {this.employeeRepository = employeeRepository;this.encrypter = encrypter;}@Override@Transactionalpublic Employee create(CreateEmployeeDTO employeeDTO) {Employee employee = this.employeeRepository.findByEmail(employeeDTO.email);if(employee != null) {throw new RuntimeException(“Já existe um funcionário com este email”);}employeeDTO.password = this.encrypter.hash(employeeDTO.password);return this.employeeRepository.create(employeeDTO);}}

>> Implementação da camada de Infra:

Como de costume em ORMs, é necessário criarmos uma classe que mapeie alguma entidade definida em um banco de dados. Neste caso, utilizaremos o Panache ORM Hibernate, portanto definiremos a classe com algumas anotações da Java Persistence API (JPA). PanacheEmployee.java:

@Entity@Table(name = “employee”)public class PanacheEmployee extends PanacheEntityBase {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)@Column(name = “id”)public Long id;@Column(name = “email”)public String email;@Column(name = “password”)public String password;@Column(name = “name”)public String name;public Employee toEmployee() {Employee employee = new Employee();employee.email = this.email;employee.password = this.password;employee.name = this.name;employee.id = this.id;return employee;}}

Obs: definiremos um método toEmployee() para fazer a conversão desta entidade do Panache para a entidade definida em nossa regra de negócio.

O Panache, por padrão, utiliza o padrão Active Record para realizar operações em banco, porém faremos um wrapper denominado Repository para realizar essas operações, note que este classe irá implementar a interface EmployeeRepository definida na camada de Data. PanacheEmployeeRepository.java:

public class PanacheEmployeeRepository implements EmployeeRepository {private final String HQL_SELECT_BY_EMAIL =“SELECT employee FROM MySqlEmployee as employee WHERE employee.email = :email”;@Override@Transactionalpublic Employee findByEmail(String email) {PanacheEmployee employee =PanacheEmployee.find(HQL_SELECT_BY_EMAIL, Parameters.with(“email”, email)).firstResult();return employee == null ? null : employee.toEmployee();}@Override@Transactionalpublic Employee create(CreateEmployeeDTO employee) {PanacheEmployee panacheEmployee = new PanacheEmployee();panacheEmployee.email = employee.email;panacheEmployee.password = employee.password;panacheEmployee.name = employee.name;panacheEmployee.persist();return panacheEmployee.toEmployee();}}

Por último, nesta camada, definiremos um adapter para a biblioteca BCrypt, que irá nos fornecer uma funcionalidade de criptografia seguindo a interface Encrypter definida na camada de Data. BCryptAdapter.java:

public class BCryptAdapter implements Encrypter {@Overridepublic String hash(String plain) {return BCrypt.hashpw(plain, BCrypt.gensalt());}}

>> Implementação da camada Main:

Aqui, definiremos uma classe que servirá para reunir os componentes criados na camada de Infra e Data. CreateEmployeeService.java:

@Singletonpublic class CreateEmployeeService {CreateEmployee createEmployee;public CreateEmployeeService() {Encrypter encrypter = new BCryptAdapter();EmployeeRepository employeeRepository = new PanacheEmployeeRepository();this.createEmployee = new CreateEmployeeImpl(employeeRepository, encrypter);}@Transactionalpublic Employee handle(CreateEmployeeDTO createEmployeeDTO) {return this.createEmployee.create(createEmployeeDTO);}}

>> Implementação da camada de Presentation:

Por último, definiremos nosso endpoint HTTP para prover nossa funcionalidade implementada para algum consumidor externo. CreateEmployeeResource.java:

@Path(“/employee”)@Consumes(MediaType.APPLICATION_JSON)@Produces(MediaType.APPLICATION_JSON)public class CreateEmployeeResource {@InjectCreateEmployeeService createEmployeeService;@POSTpublic Employee handle(@RequestBody CreateEmployeeDTO createEmployeeDTO) {return createEmployeeService.handle(createEmployeeDTO);}}

Note que nesta classe, iremos utilizar algumas anotações da especificação JAX-RS, para criar nossos endpoint’s REST.

>> Testando nossa arquitetura com ArchUnit:

Note que após toda nossa implementação, a dependência entre as camadas da nossa aplicação ficou da seguinte forma:

Todas as camadas dependem da camada de Domain, o que nos leva a entender a forte influência do Design-Driven Domain sobre a concepção do Clean Architecture.

Para testar as dependências entre as camadas, utilizaremos a biblioteca ArchUnit. Para isso, criaremos uma classe ArchTest na pasta test do nosso projeto Quarkus. ArchTest.java:

@QuarkusTestclass ArchTest {JavaClasses importedClasses = new ClassFileImporter().importPackages(“org.guilhermefaleiros”);@Testpublic void testDomainShouldNotAccessAnyOtherLayer() {ArchRule rule = noClasses().that().resideInAPackage(“..domain..”).should().accessClassesThat().resideInAnyPackage(“..infra..”, “..presentation..”, “..data..”, “..main..”);rule.check(importedClasses);}@Testpublic void testInfraShouldOnlyBeAccessedByDataAndMain() {ArchRule rule = layeredArchitecture().layer(“Infra”).definedBy(“org.guilhermefaleiros.infra..”).layer(“Main”).definedBy(“org.guilhermefaleiros.main..”).layer(“Domain”).definedBy(“org.guilhermefaleiros.domain..”).layer(“Presentation”).definedBy(“org.guilhermefaleiros.presentation..”).layer(“Data”).definedBy(“org.guilhermefaleiros.data..”).whereLayer(“Infra”).mayOnlyBeAccessedByLayers(“Main”);rule.check(importedClasses);}@Testpublic void testDataShouldOnlyBeAccessedByInfraAndMain() {ArchRule rule = layeredArchitecture().layer(“Infra”).definedBy(“org.guilhermefaleiros.infra..”).layer(“Main”).definedBy(“org.guilhermefaleiros.main..”).layer(“Domain”).definedBy(“org.guilhermefaleiros.domain..”).layer(“Presentation”).definedBy(“org.guilhermefaleiros.presentation..”).layer(“Data”).definedBy(“org.guilhermefaleiros.data..”).whereLayer(“Data”).mayOnlyBeAccessedByLayers(“Main”, “Infra”);rule.check(importedClasses);}@Testpublic void testMainShouldOnlyBeAccessedByPresentation() {ArchRule rule = layeredArchitecture().layer(“Infra”).definedBy(“org.guilhermefaleiros.infra..”).layer(“Main”).definedBy(“org.guilhermefaleiros.main..”).layer(“Domain”).definedBy(“org.guilhermefaleiros.domain..”).layer(“Presentation”).definedBy(“org.guilhermefaleiros.presentation..”).layer(“Data”).definedBy(“org.guilhermefaleiros.data..”).whereLayer(“Main”).mayOnlyBeAccessedByLayers(“Presentation”);rule.check(importedClasses);}@Testpublic void testPresentationShouldNotBeAccessedByAnyLayer() {ArchRule rule = layeredArchitecture().layer(“Infra”).definedBy(“org.guilhermefaleiros.infra..”).layer(“Main”).definedBy(“org.guilhermefaleiros.main..”).layer(“Domain”).definedBy(“org.guilhermefaleiros.domain..”).layer(“Presentation”).definedBy(“org.guilhermefaleiros.presentation..”).layer(“Data”).definedBy(“org.guilhermefaleiros.data..”).whereLayer(“Presentation”).mayNotBeAccessedByAnyLayer();rule.check(importedClasses);}}

Conclusão

Bem, foi isso, pessoal 😁. Fizemos uma aplicação simples que tinha como objetivo básico introduzir alguns conceitos como Clean Architecture, DDD, desacoplamento e muitos outros.

Ainda tem várias coisas que podem ser melhoradas neste exemplo e deixo para vocês aprimorarem como bem entenderem. Vale ressaltar que não existe uma única versão de implementação do Clean Architecture, é possível encontrar diversas implementações em contextos diferentes, mas sempre carregando os mesmos conceitos apresentados anteriormente.

Um exemplo é a arquitetura VIPER utilizada pra aplicações mobile Android e iOS, podem conferir neste artigo sobre.

Espero ter ajudado a se tornarem melhores desenvolvedores(as)! =)

Link do Repositório no GitHub: https://github.com/guilhermefaleiros/clean-quarkus

Referências:

Clean Architecture: A Craftsman’s Guide to Software Structure and Design (Robert C. Martin Series)

https://quarkus.io/

https://www.archunit.org/

https://www.udemy.com/course/tdd-com-mango

https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

https://medium.com/luizalabs/descomplicando-a-clean-architecture-cf4dfc4a1ac6

Meu nome é Guilherme Faleiros, Desenvolvedor na Mobicare, sempre em busca das melhores tecnologias e práticas da Engenharia de Software, com intuito de evoluir e repassar o conhecimento para frente.

A Mobicare e a Akross combinam os Melhores Talentos, Tecnologias de Ponta, Práticas Agile e DevOps com Capacidades Operacionais avançadas para ajudar Operadoras Telecom e grandes empresas a gerarem novas receitas e a melhorarem a experiência dos seus próprios clientes.

Se você gosta de inovar, trabalhar com tecnologia de ponta e está sempre buscando conhecimento, somos um match perfeito!

Faça parte do nosso time. 😉

--

--