Refere-se à organização e definição de regras a serem seguidas no seu projeto em si, seja ele um microserviço, monolito ou qualquer outra parte de uma solução maior, nossa ênfase está no nível do seu serviço, um serviço seu pode seguir à risca SOLID, arquitetura hexagonal e uma PoC pode seguir o famoso: faz rápido e funcionando.
De outro lado, cuidando e decidindo se temos SOA, Microserviços, Monolitos ou qual protocolo de comunicação usamos, temos a arquitetura de soluções, o que não é o foco do artigo
Modelo Baseado em Camadas
É bem comum dividirmos nosso software program em camadas, é o que fazemos na maior parte das arquiteturas de software program modernas, essa divisão tem como objetivo separar partes do código que não devem interagir muito entre si exceto por alguns pontos de contato (que podem ser outras camadas), e também garantir que exista um “meio de campo” entre certas camadas, ou seja, a interface não vai falar diretamente com o banco de dados, existe um caminho para isso.
Um dos pontos negativos desses modelos é que eles não costumam definir a obrigação ou sugestão de interfaces para comunicação com serviços externos, normalmente providers são totalmente acoplados à infraestrutura, em alguns casos, até mesmo temos DAO’s que implementam regras de persistência na camada de modelo. O problema disso é claro, nossas regras de negócio muitas vezes acabam acopladas à meras ferramentas, trocar o banco de dados exige que você mexa em um pedaço que deveria representar sua regra de negócio, o que não acontece em outros modelos como arquitetura hexagonal (a menos que você adapte seu padrão em camadas para ter abstrações significativas, o que é totalmente válido 🙂).
MVC
O MVC (Mannequin-View-Controller) é um sample arquitetural usado como um molde pra distribuição de responsabilidades em trechos de código que tratam de interfaces com o usuário (UI). Há três responsabilidades pre-estabelecidas:
- View: contém a lógica que monta a UI (telas ou equivalente) e que trata a entrada de dados, recebendo eventos do usuário (clique, digitação and so on.). As do usuário são repassadas para o Controller. A View pode buscar dados diretamente do Mannequin para exibição na UL
- Controller. o “meio de campo”, recebe interações do usuário da View e colabora com o Mannequin para enviar e obter dados, que são repassados para a View.
- Mannequin: o resto do código, que não tem a ver com UI. Realiza cálculos, regras de negócio, persistência, integrações com outros sistemas and so on. Há diversas variações como MVP, MVVM, entre outros.
Outros exemplos → MVVM
The primary thrust of the Mannequin/View/ViewModel structure appears to be that on prime of the info (”the Mannequin”), there’s one other layer of non-visual parts (”the ViewModel”) that map the ideas of the info extra carefully to the ideas of the view of the info (”the View”). It’s the ViewModel that the View binds to, not the Mannequin straight.
Arquitetura Hexagonal – Ports And Adapters
A arquitetura hexagonal é uma proposta de arquitetura de software program que segue lógicas de desenvolvimento de software program que pensam em acoplamento e coesão, basicamente, módulos de alto nível (que possuem regras de negócio) não devem depender de implementações de módulos de baixo nível (frameworks, bibliotecas de terceiros, et cetera.). Tudo que acessa o coração / domínio / regra de negócio da sua aplicação deve passar por portas, que são basicamente interfaces que representam o que aquela biblioteca fará para você, chamamos a implementação dessas interfaces de adaptadores.
Ou seja, em um sistema de login, podemos disparar um evento em uma fila de mensagens RabbitMQ que será consumido por um outro serviço de notificação, pensando em um nível um pouco mais abstrato, esquecendo bibliotecas ou ferramentas, podemos criar uma interface de publicador de eventos e dizer que vamos usar ela, ou seja, nossa lógica de negócio precisa enviar uma notificação de cadastro e um payload, independente se rabbitMQ, kafka ou outra porta está sendo usada:
personal ultimate Notifier messagePublisher;
public class TicketService {
public Ticket create(Ticket ticket){
messagePublisher.Notify(TicketEventsEnum.TICKET_CREATED);
return repository.save(ticket);
}
}
No service, a regra de negócio de criação de um ticket é exatamente essa, observe que não estou usando uma regra de rabbitMQ necessariamente, nem mesmo o seu vocabulário (normalmente usaríamos publish) o importante para a regra de criação, é enviar uma notificação!
A dependência Notifier é na realidade uma interface própria, que representa o que necessitamos, o envio de uma notificação, é uma porta.
public interface Notifier {
void Notify(Object message);
}
O Adaptador, que é basicamente uma das opções de notificadores que vc tem é a implementação actual, você poderia trocar os adaptadores e ainda assim não ter problemas no seu domínio, tendo em vista que todo adaptadores respeita a mesma interface (porta).
Think about que você está indo viajar, o núcleo da aplicação é o conteúdo essencial da sua mala – os itens vitais que você não pode deixar para trás. Os adaptadores são os diversos compartimentos e bolsos especializados na mala, cada um projetado para acomodar diferentes necessidades, você tem um plugue para tomadas da europa, outros para os estados unidos e outra que suporta o padrão adotado na ásia (não sei nem se é diferente). Da mesma forma, os adaptadores na arquitetura hexagonal conectam o núcleo da aplicação a interfaces externas variadas, como bancos de dados, interfaces de usuário e serviços externos, esses adaptadores permitem que a aplicação funcione em ambientes diversos.
Clear Structure
Não entrarei em detalhes pela sua complexidade e individualidades, mas saiba que tanto a clear structure quanto a onion se baseiam no mesmo fundamento, de proteger a camada de domínio, com os mesmos princípios de abstração por interfaces, e adaptadores implementando-as
O Deisgn orientado à Domínio (Area Pushed Design / DDD) é um conceito extenso e vai além de um sugestões sobre como dividir seu código em camadas (esse nem é o foco), comentando a maneira com que o software program é escrito, a linguagem utilizada no processo de fabricação, o que são as fronteiras entre suas entidades e regras de negócio e como elas devem ser implementadas, realmente fazendo com que a preocupação de domínio seja a central na construção de software program.
Encare o DDD como uma prescrição de metodologia e **processo** para o desenvolvimento de sistemas complexos cujo foco é mapear atividades, tarefas, eventos e dados dentro de um domínio de problema nos artefatos de tecnologia de um domínio de solução.
Apesar disso, Evans em seu livro deu diversas sugestões arquiteturais, como por exemplo, os providers, muitas vezes, mal utilizados ou interpretados. Veremos agora algumas sugestões e pontos do autor.
DDD – Sugestões Arquiteturais → e de design de código
Antes de tudo, acho importante definir o que é o domínio de uma aplicação:
Domínio:
No contexto de Engenharia de Software program é o “conhecimento” utilizado em uma determinada área de aplicação, um campo específico para qual o sistema foi desenvolvido, ou seja, os problemas, regras e soluções que envolvem uma parte da aplicação, apesar disso, muitas vezes nos referimos ao domínio de negócio (núcleo de regras e conhecimentos que envolvem o negócio) somente como domínio, leve isso em consideração, porém tenha em mente que uma aplicação tem diversos domínios.
Quando o código relacionado ao domínio é distribuído por uma porção tão grande de outros códigos (espalhado), torna-se extremamente difícil distingui-los e raciocinar. Alterações superficiais na interface do usuário podem realmente alterar a lógica de negócios (alterações vazam para onde não devem).
Assim sendo:
Isole o modelo do domínio e a lógica de negócios e elimine qualquer dependência que eles possam ter na infraestrutura, na interface do usuário ou mesmo na lógica do aplicativo que não seja lógica de negócios.
Particione um programa complexo em camadas. Desenvolva um design dentro de cada camada que seja coeso e que dependa apenas das camadas abaixo. Concentre todo o código relacionado ao modelo do domínio em uma camada e isole-o do código da interface do usuário, do aplicativo e da infraestrutura. Os objetos de domínio, livres da responsabilidade de se exibir, de se armazenar, de gerenciar tarefas do aplicativo, e assim por diante, podem se concentrar em expressar o modelo do domínio. Isso permite que um modelo evolua para se tornar rico e limpo o suficiente para capturar o conhecimento essencial do negócio e colocá-lo para funcionar, sempre que uma regra de negócio surgir, o modelo de domínio deve ser o necessário por implementá-la, quem deve se adaptar às regras de negócio é a implementação, e nunca o contrário.
Dito isso, colocar as responsabilidades certas no domínio não significa que o modelo deve ser anêmico, o modelo pode (e deve) manter regras e formas para que seu escopo seja válido. Ou seja, fazemos o possível para que uma entidade de domínio nasça e proceed sempre de acordo com suas regras de negócio.
Modelos Anêmicos – Um problema
Conceito muito difundido no artigo Anemic Area Mannequin, de Martin Fowler.
Quando falamos de modelos de domínio anêmicos dizemos de modelos onde as regras de negócio associadas à uma entidade é externa à própria entidade. Temos uma classe pedido mas o método para verificar se o pedido contém itens ou não está em um “service”, que acaba sendo uma classe que possui regras que poderiam existir dentro de uma própria entidade (se contiver somente o seu comportamento).
Courses que possuem somente atributos são courses de domínio anêmicas, idealmente, uma classe deve conter comportamento e atributos.
Podemos chamar courses JAVA ou C# que são totalmente desacoplada de outras bibliotecas ou framewrks de POCO (no C#) ou POJO (no JAVA). Por serem códigos puros escritos em java ou c#, que não deviram de uma classe base e nem retornam ou utilizam de tipos especiais, ou seja, são courses simples que sabem apenas de seu domínio, devemos sempre seguir os princípios da ignorância da infraestrutura e ignorância da persistência para essas courses.
Portanto, as entidades não devem ser associadas aos modos de exibição do cliente pois, no nível da interface do usuário, alguns dados podem ainda não ter sido validados. É por esse motivo que o ViewModel existe. O ViewModel é um modelo de dados exclusivamente para necessidades de camada de apresentação. As entidades de domínio não pertencem diretamente ao ViewModel. Em vez disso, você precisa converter entre entidades de domínio e ViewModels e vice-versa. – *Projetar um microsserviço orientado a DDD, Microsoft*
Refatorando um Domínio anêmico
Atributos distantes do comportamento
Para começar, recomendo ler o caso 1 de 3. Refatoração → Casos Usuais, depois volte aqui. De maneira geral, courses devem guardar dentro de si atributos e comportamentos, se você possui comportamentos que agem sobre os atributos de uma classe específica, costuma fazer sentido encapsulá-los dentro da classe.
Exemplo:
Class ComprarIngressoService{
void comprar(Pessoa pessoa, Evento evento){
...
if(pessoa.idade<18){
// menor de idade
}
if(pessoa.getCadastro()=="ativo"){
//
}
}
}
Esse tipo de validação é aparentemente inofensiva, contudo, frágil, pode causar diversas repetições no código e aumentar pontos de contato para uma possível alteração, em alguns casos, esse tipo de erro piora muito a leitura. Faz sentido que a classe Pessoa cuide de propriedades das pessoas, brand, a refatoração a seguir é possível:
Class ComprarIngressoService{
void comprar(Pessoa pessoa, Evento evento){
...
if(pessoa.maiorDeIdade()){
// menor de idade
}
if(pessoa.estaAtiva()){
//
}
}
}
Construtores, Builders e falta de amor aos erros de compilação
Um grande motivo para escrevermos códigos que são fortemente tipados é a possibilidade de perceber erros em tempo de compilação, erros que impedem que façamos coisas que não fazem sentido dado o contexto do que estamos tentando fazer, a semântica de string, por exemplo, entendida como cadeia de caracteres, não permite a soma de números a ela (Some um à kaue).
Dito isto, grande parte das courses de domínio não validam seu estado, muitas vezes nem em sua criação. É comum ver por ai courses com construtores vazios e códigos setters públicos (pois getters e setters teoricamente protegem o encapsulamento) isso por si só não garante que uma classe irá ser usada como esperada, veja o exemplo a seguir.
class Pessoa{
Pessoa(){
// construtor vazio, no java é opcional
}
@Getter
@Setter // simulando o lombok, mas pode imaginar que são métodos getter e setters públicos
personal Lengthy id;
@Getter
@Setter
personal String nome;
@Getter
@Setter
personal Int peso;
}
// em algum outro lugar:
Pessoa kaue = new Pessoa();
kaue.setId(1);
kaue.setNome("kaue");
kaue.setPeso(65);
// teoricamente kaue está tranquilo levando em conta que todos os campos foram preenchidos, mas e o seguinte?
Pessoa douglas = new Pessoa();
douglas.setPeso(70);
Não existe erro de compilação e nem de execução (POR ENQUANTO) aqui.
É óbvio que os setters deveriam validar se os campos foram preenchidos de seguindo um certo padrão e que faltam métodos para lidar com o objeto pessoa como indicado no ponto anterior, o modelo está anêmico, mas esse não é o foco, criamos um objeto de uma Pessoa chamado douglas, que possui apenas seu peso definido, o que provavelmente não faz sentido quando pensamos na criação de uma pessoa em um sistema, deveríamos (dependendo do negócio) ao menos forçar o preenchimento de id e nome.
class Pessoa{
Pessoa(String id, String nome){
// único construtor recebendo os campos opcionais.
this.id=id;
this.nome=nome;
}
@Getter
// setter não existe mais
personal Lengthy id;
@Getter
// setter pode até existir, mas nesse caso não vou criar.
personal String nome;
@Getter
@Setter
personal Int peso;
}
// em algum outro lugar:
Pessoa kaue = new Pessoa(1,"kaue");
kaue.setPeso(65);
Pessoa douglas = new Pessoa(); // erro
douglas.setPeso(70);
Mas e courses builders? Também não é incomum ver builders que esquecem de implementar os campos obrigatórios, para nossa felicidade, é algo simples de ser resolvido.
PessoaBuilder pessoaBuilder = new PessoaBuilder(1,"kaue"); // construtor do BUILDER tem em si os parâmetros necessários para criar a classe que constrói
// se o método para pegar o builder for um método estático, só passar em seu parâmetro
Pessoa kaue = pessoaBuilder
.withPeso(70)
.construct();
Usando o lombok @builder, podemos fazer:
import lombok.Builder;
@Builder(builderMethodName = "hiddenBuilder")
public class Individual {
@NotNull
personal String title;
personal String surname;
public static PersonBuilder builder(String title) {
return hiddenBuilder().title(title);
// o nome desse builder interno é arbitrário
}
}
//
Individual p = Individual.builder("Kaue").surname("Surname").construct();
Essa tática, usando o lombok ou não, tem alguns problemas e normalmente não faz sentido em courses que tem muitos parâmetros obrigatórios e até mesmo em algumas courses simples, pois depreca, mesmo que um pouco, uma das grandes vantagens que a classe builder tem, a visibilidade, think about isso:
Endereco e = Endereco.builder("Osvaldo Albherto", "Parque Bitaru", "42", "Abilio")
.complemento("Ap 1")
.maisInformacoes("Pode entregar professional vizinho")
.construct();
Somente lendo esse código, você só consegue ter certeza do complemento e maisInformacoes, os outros campos não são tão visíveis, ainda assim, como opinião pessoal, prefiro por ter esse código, que se torna um pouco menos visível mas garante o uso correto da classe, mostrando erros de compilação na própria IDE caso os atributos obrigatórios não estejam preenchidos.
Modelos Ricos: como lidar com dependências excessivas
Se sua classe POJO de domínio necessitar de bibliotecas ou outras dependências (faça-as serem interfaces 🙏), instanciá-la ficará extremamente inconveniente, para isso existe o Design Sample: Manufacturing unit
Design Sample: Manufacturing unit
F*actories são métodos (ou courses) que possuem como retorno a criação de um outro objeto*, em casos mais simples, podem ser métodos estáticos dentro da própria classe, em casos mais complexos, onde teremos diferentes dependências a serem injetadas nas courses de domínio atráves de um framework ou container de injeção de dependência, como o Spring faz, podemos usar courses.
Think about a existência de uma classe usuário, que necessita que seu próprio e mail seja validado, e para isso, você quer usar uma biblioteca x ou y, você, respeitando princípios básicos, criará uma interface a qual Usuário dependerá, e fará com que a injeção de dependência passe a você uma instância do validador em algum momento, isso irá se tornar extremamente inconveniente muito rápido, portanto, podemos fazer:
public class Usuario {
personal String e mail;
personal EmailValidator emailValidator;
public Usuario(String e mail, EmailValidator emailValidator) {
this.e mail = e mail;
this.emailValidator = emailValidator;
}
public boolean isEmailValid() {
return emailValidator.isValid(e mail);
}
// Outros métodos da classe Usuario
}
public class UsuarioFactory {
personal ultimate EmailValidator emailValidator;
// Construtor com injeção de dependência!
public UsuarioFactory(EmailValidator emailValidator) {
this.emailValidator = emailValidator;
}
// Método para criar instância de Usuario usando o validador de e-mail fornecido pelo Spring (ou pelo seu framework de DI)
public Usuario createUsuario(String e mail) {
return new Usuario(e mail, emailValidator);
}
}
E os Companies?
Evans Descreve em seu livro três tipos de providers:
Utility Service:
- Fornece para o usuário operações que o seu software program pode executar, e controla a execução dessas operações através de chamadas a métodos de objetos das outras camadas (domínio, infraestrutura, and so on.). É importante dizer que a Utility Service não contém regras de negócios ou conhecimento do domínio, sendo assim, ela apenas coordena as chamadas a métodos de outras camadas e mantém o estado que reflete o progresso de uma operação para o usuário.
Utility Layer: Defines the roles the software program is meant to do and directs the expressive area objects to work out issues. The duties this layer is accountable for are significant to the enterprise or needed for interplay with the applying layers of different programs. This layer is saved skinny. It doesn’t comprise enterprise guidelines or information, however solely coordinates duties and delegates work to collaborations of area objects within the subsequent layer down. It doesn’t have state reflecting the enterprise state of affairs, however it might probably have state that displays the progress of a activity for the consumer or this system. – Evans DDD
Area Companies:
- Fornece para a Utility Service métodos que permitam a execução de operações sobre os objetos de Domínio (camada mais interna). Embora seja comum representar grande parte dos conceitos e regras principais do negócio aqui, o splendid é que esses detalhes sejam representados diretamente nos Area Fashions. Sendo assim, o Area Service deve chamar e controlar a execução de métodos dos objetos do Area Mannequin quando não é trivial ou lógico declarar um método diretamente no modelo de domínio
As vezes, a situação simplesmente não se trata de uma coisa.
Alguns conceitos do domínio não são naturais para serem modelados na forma de objetos.
Forçar a funcionalidade do domínio necessária para que ela seja a responsabilidade de uma Entidade ou Objeto de Valor distorce a definição de um objeto baseado em modelos ou adiciona objetos artificiais sem sentido.
Assim sendo:
Quando um processo ou transformação significativa no domínio não é uma responsabilidade pure de uma Entidade ou Objeto de Valor, adicione uma operação no modelo como uma interface autônoma declarada como Serviço. Defina um contrato de serviço, um conjunto de asserções sobre interações com o Serviço. (Veja “asserções”) Torne essas asserções participantes da Linguagem Onipresente de um Contexto Delimitado específico. Dê um nome ao Serviço, que também se torne parte da Linguagem Onipresente.
Evans – DDD
Infrastructure Companies:
- Fornece métodos que permitem a execução de operações sobre a infraestrutura na qual o software program está sendo executado. Isso significa que esses serviços tem conhecimento sobre detalhes das implementações concretas da infraestrutura tais como: acesso a bancos de dados, acesso a rede, controle de operações de IO, acesso a {hardware} and so on. Geralmente esse service é utilizado pelos Utility Companies para complementar e auxiliar suas operações, por exemplo, fornecer um método que permita a criação e controle de um buffer para realizar obtain de arquivos.
Contrapontos:
💡 Regardless, in case your microservice or Bounded Context may be very easy (a CRUD service), the anemic area mannequin within the type of entity objects with simply information properties could be ok, and it may not be value implementing extra advanced DDD patterns. In that case, it is going to be merely a persistence mannequin, as a result of you might have deliberately created an entity with solely information for CRUD functions.
Design a microservice domain model – Microsoft
Some folks say that the anemic area mannequin is an anti-pattern. It actually is determined by what you might be implementing. If the microservice you might be creating is easy sufficient (for instance, a CRUD service), following the anemic area mannequin it’s not an anti-pattern. Nonetheless, if that you must sort out the complexity of a microservice’s area that has lots of ever-changing enterprise guidelines, the anemic area mannequin could be an anti-pattern for that microservice or Bounded Context. In that case, designing it as a wealthy mannequin with entities containing information plus habits in addition to implementing further DDD patterns (aggregates, worth objects, and so on.) might need enormous advantages for the long-term success of such a microservice.
Aqui entendemos uma coisa que deve ser clara, não existe bala de prata na computação, faz sentido abstraírmos o SPRING, Controllers, Companies e outras funcionalidades ou entedemos que nossa aplicação nasce acoplada ao SPRING e morre com ele?
Aqui, tudo cabe à você entender pontos, contrapontos e o seu contexto, no seu caso. Se sua aplicação só existe junto à infraestrutura de uma biblioteca, talvez não haja motivo para desacoplá-la, se você não vê perspectivas para deixar de usar lombok, não necessariamente precisa fazer seu modelo de domínio POJOS realmente puras use seu lombok, e seja feliz. Um projeto simples ou que necessita ser entregue muito rapidamente não usar de conceitos como Arquitetura Hexagonal, DDD, CQRS ou qualquer outro sample não se traduz emprojeto simples ou que significa código ruim.
The Software Architecture Chronicles
Esse weblog, essa crônica em específico – é maravilhosa!! Se estiver off, procure no wayback machine.
Area Pushed Design, Eric Evans
Anemic Domain Model, Martin Fowler (Cosigned by Evans)
Sumário de Padrões e Definições do DDD – Traduzido por Ricardo Pereira Dias
Projetando um microsserviço orientado a DDD