A arquitetura hexagonal, também conhecida como arquitetura de ports and adapters, é um padrão arquitetural que busca criar sistemas flexíveis, testáveis e de fácil manutenção. Ela foi introduzida por Alistair Cockburn em 2005 como uma alternativa ao padrão arquitetural em camadas.
A principal ideia por trás da arquitetura hexagonal é separar a lógica de negócio central da aplicação das preocupações externas, como interfaces de usuário, banco de dados, serviços externos e integrações. Isso é feito por meio da divisão em três principais componentes: domínio, adaptadores e interfaces.
- Domínio:
O domínio é o núcleo da aplicação e contém a lógica de negócio e as regras do sistema. Ele representa o cerne da aplicação e é independente de qualquer tecnologia externa. Aqui, as entidades, os serviços e os casos de uso são definidos. Essa camada deve ser pura e não depender de detalhes de implementação externa.
- Adaptadores:
Os adaptadores são responsáveis por conectar o domínio às tecnologias externas. Existem dois tipos principais de adaptadores: os adaptadores de entrada (inbound adapters) e os adaptadores de saída (outbound adapters).
- Adaptadores de entrada:
São responsáveis por receber as requisições externas e adaptá-las para o formato compreendido pelo domínio. Eles lidam com as interfaces de usuário, como interfaces web, APIs, CLI (Command Line Interface), entre outros. A função desses adaptadores é converter as requisições externas em chamadas para os casos de uso do domínio.
- Adaptadores de saída:
São responsáveis por fornecer implementações concretas das interfaces externas, como banco de dados, serviços de terceiros, sistemas legados, etc. Eles encapsulam a lógica necessária para persistir dados e realizar integrações externas. Os adaptadores de saída também são responsáveis por converter os resultados do domínio em formatos compreendidos pela tecnologia externa.
- Interfaces:
As interfaces definem os contratos entre o domínio e o mundo externo. Elas permitem a comunicação entre os adaptadores de entrada e os adaptadores de saída. As interfaces são implementadas por adaptadores concretos e são usadas para conectar a lógica de negócio com os componentes externos.
A arquitetura hexagonal busca manter o domínio livre de dependências externas e detalhes de implementação. Isso permite testar o domínio de forma isolada, sem a necessidade de interfaces externas. Os testes podem ser executados utilizando mocks ou stubs para simular as interfaces externas, garantindo a validação correta da lógica de negócio.
Além disso, a arquitetura hexagonal facilita a evolução e a substituição de componentes externos. Se um banco de dados precisa ser alterado, por exemplo, basta criar um novo adaptador de saída que implemente a mesma interface, mantendo a lógica de negócio inalterada.
Em resumo, a arquitetura hexagonal é uma abordagem que promove a separação clara das responsabilidades dentro de uma aplicação.
Exemplo prático em Java com framework Spring
Vamos supor que estamos construindo uma aplicação de gerenciamento de usuários, onde podemos cadastrar, buscar e excluir usuários. Seguindo a arquitetura hexagonal, teremos três principais pacotes em nossa aplicação:
Pacote do Domínio:
User: Representa a entidade de usuário, contendo seus atributos e métodos relacionados.
UserRepository: Define a interface do repositório de usuários, especificando as operações CRUD necessárias.
UserService: Define a interface do serviço de usuário, com os casos de uso relacionados ao gerenciamento de usuários.
Pacote dos Adaptadores:
UserAdapter: Implementa a interface UserRepository e é responsável por lidar com a persistência de dados no banco de dados. Utilizaremos o Spring Data JPA para simplificar a implementação do repositório.
UserController: Implementa as interfaces de entrada, recebendo as requisições externas através de endpoints REST. Ele é responsável por receber as requisições, adaptá-las para o formato adequado e invocar os casos de uso correspondentes no UserService.
Pacote das Interfaces:
UserInputBoundary: Define a interface para os casos de uso do UserService, representando as ações que podem ser realizadas na entidade de usuário.
UserOutputBoundary: Define a interface para os retornos dos casos de uso do UserService, especificando as informações que serão devolvidas como resposta.
Dentro do pacote do Domínio, teremos as seguintes classes:
User.java
public class User {
private String id;
private String name;
// Outros atributos e métodos relevantes
}
UserRepository.java
public interface UserRepository {
User save(User user);
User findById(String id);
void delete(String id);
}
UserService.java
public interface UserService {
User createUser(User user);
User getUserById(String id);
void deleteUser(String id);
}
No pacote dos Adaptadores, teremos:
UserAdapter.java
@Repository
public class UserAdapter implements UserRepository {
private final UserRepositoryImpl userRepositoryImpl;
// Construtor
@Override
public User save(User user) {
return userRepositoryImpl.save(user);
}
@Override
public User findById(String id) {
return userRepositoryImpl.findById(id);
}
@Override
public void delete(String id) {
userRepositoryImpl.deleteById(id);
}
}
UserController.java
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
// Construtor
@PostMapping
public UserDTO createUser(@RequestBody UserDTO userDTO) {
User user = UserMapper.toEntity(userDTO);
User createdUser = userService.createUser(user);
return UserMapper.toDTO(createdUser);
}
@GetMapping("/{id}")
public UserDTO getUserById(@PathVariable String id) {
User user = userService.getUserById(id);
return UserMapper.toDTO(user);
}
@DeleteMapping("/{id}")
public void deleteUser(@PathVariable String id) {
userService.deleteUser(id);
}
}
Por fim, no pacote das Interfaces, teremos:
UserInputBoundary.java
public interface UserInputBoundary {
User createUser(User user);
User getUserById(String id);
void deleteUser(String id);
}
UserOutputBoundary.java
public interface UserOutputBoundary {
UserDTO createUser(UserDTO userDTO);
UserDTO getUserById(String id);
void deleteUser(String id);
}
Essa é apenas uma estrutura básica da implementação da arquitetura hexagonal em uma aplicação Java usando o Spring. É importante ressaltar que existem outras camadas e componentes que podem ser adicionados para uma aplicação mais completa, como validações, mapeamentos, tratamento de exceções, entre outros.
Além disso, a configuração do Spring, como a injeção de dependências e a definição das interfaces de entrada e saída, também são pontos importantes a serem considerados, mas que não foram abordados aqui por questões de simplicidade.
Lembrando que a arquitetura hexagonal oferece a flexibilidade de adaptar as interfaces externas e componentes de persistência sem afetar a lógica de negócio, facilitando a manutenção e os testes automatizados.
Esse exemplo serve como ponto de partida para a implementação de uma arquitetura hexagonal em sua aplicação Java usando o Spring. É sempre importante adaptar e ajustar de acordo com as necessidades do seu projeto específico.
Feito!