Arquitetura hexagonal é um estilo que fala sobre camadas de seus objetos de uma forma que isola sua lógica central de elementos externos. A lógica principal é a parte específica do seu negócio, e os elementos externos são como pontos de integração, por exemplo, bancos de dados, APIs externas, interfaces de usuário e outros. Ele divide o software nas partes interna e externa. As partes internas contêm a lógica do Core Business e a camada Domain (explicada em Layered Architecture). A parte externa consiste em interface do usuário, banco de dados, mensagens e outras coisas. Partes internas e externas se comunicam entre si por meio de portas e adaptadores.
Benefícios
O software desenvolvido usando esta arquitetura é independente de canais e pode suportar múltiplos canais
Fácil de trocar os pontos de integração de entrada e saída
Testar o software torna-se fácil porque podemos simular pontos de integração facilmente
Implementação em Java
Como explicado acima, a arquitetura hexagonal é mais em torno de portas e adaptadores. Em Java, as interfaces implementam as portas e a classe de implementação funciona como os adaptadores. Então, vamos dar uma olhada em um exemplo simples usando o Spring Boot e ver como esse estilo pode ser aplicado a este framework.
Nesta aplicação, temos a funcionalidade para criar / visualizar detalhes do empregado. A lógica do Core Business está no EmployeeService e o domínio é umEmployee. Então, essas serão consideradas partes internas
@Service
public class EmployeeService {
@Autowired
private EmployeeRepositoryPort employeeRepository;
public void create(String name, String role, long salary){
employeeRepository.create(name, role, salary);
}
public Employee view(Integer userId){
return employeeRepository.getEmployee(userId);
}
}
Portanto, agora, esse aplicativo pode expor essa funcionalidade via REST ou Messaging. Assim, criamos o EmployeeControllerAdapter para expor endpoints REST, que implementa o EmployeeUIPort.
@RestController
@RequestMapping("/employees/")
public class EmployeeControllerAdapter implements EmployeeUIPort{
@Autowired
private EmployeeService employeeService;
@Override
public void create(@RequestBody Employee request) {
employeeService.create(request.getName(), request.getRole(), request.getSalary());
}
@Override
public Employee view(@PathVariable Integer id) {
Employee employee = employeeService.view(id);
return employee;
}
}
public interface EmployeeUIPort {
@PostMapping("create")
public void create(@RequestBody Employee request);
@GetMapping("view/{id}")
public Employee view(@PathVariable Integer userId);
}
Como parte da lógica de negócios,EmployeeService também precisa chamar o banco de dados, que é, novamente, um ponto de integração (parte externa), portanto, criamos EmployeeRepositoryPort, e EmployeeServiceAdapterimplementamos essa porta.
@Service
public class EmployeeServiceAdapter implements EmployeeRepositoryPort {
@PersistenceContext
private EntityManager entityManager;
@Transactional
@Override
public void create(String name, String role, long salary) {
Employee employee = new Employee();
employee.setName(name);
employee.setRole(role);
employee.setSalary(salary);
entityManager.persist(employee);
}
@Override
public Employee getEmployee(Integer userId) {
return entityManager.find(Employee.class, userId);
}
}
public interface EmployeeRepositoryPort {
void create(String name, String role, long salary);
Employee getEmployee(Integer userId);
}
Então, vemos como EmployeeService usou a EmployeeUIPortporta para expor seu serviço e EmployeeRepositoryPort interagir com o banco de dados. Além disso, EmployeeControllerAdapter eEmployeeServiceAdapter ajuda a integrar com APIs REST e DB.
Conclusão
Para resumir, a arquitetura hexagonal é uma abordagem usada para dividir o aplicativo em partes internas e externas . Eles estão conectados através de portas (expostas pelo lado de dentro) e adaptadores (implementados pelo lado de fora). Portanto, ao aplicar essa abordagem, o código do caso de uso principal permanece intacto e pode servir a vários canais, suportando diferentes protocolos. Também ajuda a tornar o aplicativo testado com facilidade. No entanto, sugiro não implementar essa arquitetura completamente para todo o aplicativo, mas usar interfaces e adaptadores seletivamente.
O código de todos os exemplos acima pode ser encontrado no GitHub .
Agora vamos falar sobre classes imutáveis em Java e mostrar como tornar a classe imutável.
A classe é chamada imutável se for impossível alterar seu estado e conteúdo após a inicialização (criação).
Os benefícios de tais classes são:
segurança: você, como desenvolvedor, pode ter certeza de que ninguém é capaz de mudar seu estado;
thread-safety: o mesmo mencionado acima também é real para o ambiente multithread;
armazenável em cache: as instâncias podem ser armazenadas em cache facilmente pelo cache da VM ou pela implementação customizada, pois temos 100% de certeza de que seus valores não serão alterados;
hashable: tais classes podem ser seguramente colocadas dentro das coleções de hash (como HashMap, HashSetetc) – é claro, se os métodos equals()& hashCode()são substituídos de maneira apropriada.
Então, vamos tentar implementar a classe imutável pegando o mutável e mudando sua implementação para o imutável passo a passo.
Considere, temos a seguinte classe POJO típica Hobbit:
public class Hobbit {
private String name;
private Address address;
private List<String> stuff;
// getters:
public String getName() {
return name;
}
public Address getAddress() {
return address;
}
public List<String> getStuff() {
return stuff;
}
}
Parece melhor, mas agora o usuário de nossa classe não tem chances de definir os valores para campos de classe. Então, vamos fornecer o construtor apropriado:
public class Hobbit {
private String name;
private Address address;
private List<String> stuff;
// all args constructor:
public Hobbit(String name, Address address, List<String> stuff) {
this.name = name;
this.address = address;
this.stuff = stuff;
}
// getters:
public String getName() {
return name;
}
public Address getAddress() {
return address;
}
public List<String> getStuff() {
return stuff;
}
}
Nós terminamos agora? Ainda não, restam vários problemas. Primeiro de tudo, nossa imutabilidade poderia ser hackeada usando um dos princípios da OOP – herança. Deixe-me demonstrar isso com um exemplo simples:
public final class Hobbit {
private String name;
private Address address;
private List<String> stuff;
// all args constructor:
public Hobbit(String name, Address address, List<String> stuff) {
this.name = name;
this.address = address;
this.stuff = stuff;
}
// getters:
public String getName() {
return name;
}
public Address getAddress() {
return address;
}
public List<String> getStuff() {
return stuff;
}
}
Agora, se executarmos a Hackaula mais uma vez, a saída estará correta:
Frodo Bolseiro
Imutabilidade foi hackeada!
Frodo Bolseiro
Há mais uma questão interessante: como sabemos, em Java quando passamos o valor de um objeto, estamos passando a referência a ele. Assim, o estado dos objetos poderia ser alterado pelos efeitos colaterais. Vamos voltar ao código para ver isso:
Há mais uma etapa opcional a ser executada, mas se todos os outros pré-requisitos forem atendidos, isso não é necessário. De qualquer forma, vamos mencionar isso:
6. marcando todos os campos de classe como final (opcional)
import java.util.ArrayList;
import java.util.List;
public final class Hobbit {
private final String name;
private final Address address;
private final List<String> stuff;
// all args constructor:
public Hobbit(String name, Address address, List<String> stuff) {
this.name = name;
this.address = new Address(address.getCountry(), address.getCity());
this.stuff = new ArrayList<>(stuff);
}
// getters:
public String getName() {
return name;
}
public Address getAddress() {
return new Address(address.getCountry(), address.getCity());
}
public List<String> getStuff() {
return new ArrayList<>(stuff);
}
}
E é isso, estamos prontos – nossa Hobbitclasse é imutável agora.
Vamos resumir os passos:
remover setters;
adicionando todo o construtor args;
marcando a classe de modo finala protegê-la de ser estendida;
inicializar todos os campos mutáveis não primitivos via construtor executando uma cópia profunda;
executando clonagem do objeto mutável não-primitivo retornado nos métodos getter;
marcando todos os campos de classe como final(etapa opcional).
Claro, existem outras maneiras de tornar a classe imutável (por exemplo, usando o padrão Builder), mas está fora do escopo da história atual.