Arquitetura limpa e Typescript: Entidades

Definindo e implementando o domínio da aplicação

Filipe Mata
10 min readSep 13, 2020
Photo credit: mikecohen1872 on VisualHunt / CC BY

Eis aí dois termos hypados no desenvolvimento de sistemas: Arquitetura limpa e Typescript. Nada melhor então do que criar um artigo unindo esses dois termos, não é mesmo?! Mas meu objetivo aqui é muito mais do que só aumentar o hype em torno desses temas que muitos desenvolvedores já estão cansados de ler e de saber. O intuito deste artigo, e dos próximos 3, é repassar (ou pelo menos tentar) um pouco do conhecimento que eu adquiri sobre arquitetura limpa ao longo de alguns anos. A maneira mais fácil de fazer isso, pelo menos pra mim, é criando um projeto em Typescript e mostrando a mágica acontecer.

Para quem ainda não conhece, Arquitetura Limpa é um termo cunhado pelo engenheiro Robert Martin. Basicamente este conceito especifica que um bom arquiteto de softwares não deve se preocupar com os detalhes de um sistema antes da definição e implementação de suas regras de negócio. Mas o que são esses detalhes? Bancos de dados, frameworks, drivers, bibliotecas e todo o tipo de tecnologia que irá suportar uma aplicação.

Nessa hora você deve estar se questionando: “Como eu crio um sistema sem antes definir a estrutura do meu banco de dados? Impossível”. Ao contrário, é totalmente possível. E digo mais, é a decisão mais inteligente que você irá tomar como arquiteto de sistemas.

Mas se eu não começar um projeto definido suas bibliotecas, rotas e BDs, por onde eu devo começar? Do jeito mais simples: Definindo as regras de negócios do seu core business e da sua aplicação.

Para ajudar a entender um pouco melhor como essa dinâmica funciona, vamos imaginar que você foi contratado para desenvolver uma loja online e que sua primeira entrega será o módulo de pedidos. O primeiro passo para isso é definir quais são as regras mais gerais e de mais alto nível. Tais regras são definidas em Clean Architecture como entidades.

É importante se ater a algumas diferenças que podem existir nas definições das entidades, também conhecidas como domínio da aplicação. Para o uncle Bob entidades podem ser classes com métodos, estruturas de dados ou simples objetos utilizados pelos casos de uso da aplicação. São as regras de negócio mais gerais e críticas do seu negócio. No entanto, considerando alguns conceitos de DDD, iremos dividir o tipo dos objetos de domínio em Value Objects e Entidades.

Observando o contexto do negócio identificamos que existem pelo menos os seguintes objetos de domínio:

  • Address: É um value object que representa um endereço. Como todo value object , sua unicidade é garantida através da comparação de todas as suas propriedades.
  • Customer: Representa o cliente da loja e todas as regras de domínio pertinentes ao cadastro do mesmo, como, por exemplo, alguma validação que exija que o cliente seja maior de idade. Um Cliente contém um identificador único no sistema além de possuir um Endereço de cadastro.
  • Order: Representa algum pedido que o cliente irá realizar na loja. Está associado a um comprador (Customer), é composto por um ou mais items (LineItem), possui um endereço de entrega vinculado (Address) e em algum momento do seu ciclo de vida poderá conter uma fatura (Invoice). Estes relacionamentos de composição entre Order, LineItem e Invoice exemplificam o que é definido no DDD como Agregado. A raiz deste agregado é a entidade Order, ou seja, é somente por meio dela que é possível criar um item de pedido (LiteItem) ou adicionar uma fatura (Invoice).
  • LineItem: É um item do pedido que uma referência a um produto comprado bem como sua quantidade. Como toda entidade, possui um identificador único, no entanto, por fazer parte do agregado da entidade Order, toda sua existência depende da entidade Order. Ex: Só é possível listar e remover um item de pedido (LineItem) a partir da entidade raiz do agregado, no caso a entidade Order.
  • Invoice: É um value object que representa os dados de faturamento de um pedido. Assim como a entidade LineItem, também está intimamente atrelada a entidade Order através do agregado. Ou seja, uma fatura só existe se o pedido existir, bem como só é possível recuperar os dados de fatura de um pedido através do próprio pedido.
  • Product: Talvez seja a entidade mais básica da aplicação, contendo as informações de um determinado produto como nome, descrição, preço e, claro, um identificador único.

Abaixo vemos uma versão simplificada do diagrama entidade-relacionamento do sistema em questão:

Diagrama UML de classes do módulo de pedidos

Com o domínio da aplicação definido é hora de colocar a mão na massa. O primeiro passo é definir a estrutura do seu projeto em Typescript.

Para o projeto em questão a divisão em camadas é evidenciada por uma divisão equivalente entre diretórios, como é mostrado na imagem abaixo:

Estrutura de diretórios do projeto

O diretório que iremos trabalhar neste artigo é o diretório entities. Neste diretório estão todas regras de negócio do domínio da aplicação.

Abaixo é mostrada a classe abstrata que representa os value objects da aplicação. Observe o método equals(vo: ValueObject<T>): boolean . Este método define se dois value objects são ou não iguais através da comparação de suas propriedades.

A seguir é mostrada uma classe abstrata para as entidades da aplicação. Esta classe implementa todos os comportamentos comuns às entidades concretas, como definição de um identificador único e comparação com outra entidade.

Algumas observações devem ser feitas com relação a classe Entity:

  • Entity<T> é uma classe abstrata, portanto, não pode ser instanciada diretamente. No entanto, você pode criar uma subclasse de Entity, o que faz total sentido, já que a existência de uma entidade só faz sentido se for de um tipo específico, como por exemplo class Order extends Entity<OrderProps>
  • O método equals(entity: Entity<T>): boolean define o mesmo comportamento para se comparar qualquer entidade. Primeiro é verificada se a referência das duas entidades comparadas é a mesma. Caso não seja o identificador único das entidades é comparado.
  • As propriedades da entidade são armazenadas em this.props . Isto é feito para que deixemos para a subclasse a decisão de quais getters e setters serão definidos.
  • O id da entidade é definido com o prefixo readonly , ou seja, uma vez que a entidade seja criada o id não pode ser modificado.
  • O id da entidade é opcional pois quando uma entidade for recuperada através de um gateway o id deve ser repassado ao construtor, já quando uma entidade for criada, a mesma deve se encarregar de gerar um novo id.
  • O id da entidade tem um tipo específico UniqueEntityId . Assim, eu não preciso me importar se o id é uma string ou um inteiro. Esta classe encapsula toda a regra envolvendo a definição e comparação de identificadores.
  • Para criação da entidade é utilizado o padrão static factory method em conjunto com um construtor privado. Assim garantimos que uma nova entidade não será instanciada sem antes passar por validações de algumas pré-condições.
  • Quando o id não é repassado na construção da entidade, o mesmo é gerado através de um static method factory de geradores de ids. Esta decisão é tomada para que a entidade não conheça detalhes de infraestrutura ao gerar seu identificador.
  • É possível identificar se uma entidade é nova ou não, ou seja, se a mesma foi recém criada ou recuperada através de um repositório de dados. Isto é possível a partir de uma flag isNew recebida no construtor. A responsabilidade de definir esta flag é do componente que está construindo a entidade.
  • É utilizado o mecanismo de Proxy do Javascript para customizar o comportamento dos setters da entidade, de modo que, toda vez que uma propriedade é alterada em uma entidade que não é nova, o nome da propriedade é armazenado em uma lista de propriedades sujas. Esta lista será útil na hora de implementarmos os controles de persistência da entidade, na camada de adaptadores.

Abaixo é mostrado com mais detalhes a estrutura e os comportamentos envolvendo a geração e atribuição de identificadores únicos para entidades.

Como visto acima, a classe UniqueEntityId encapsula um tipo primário para um id, seja ele um número ou uma string. A construção da classe é feita através da extensão de um identificador genérico mostrado a seguir:

A class Identifier utiliza de um tipo genérico para definir um identificador único. Algo que deve ser observador é o método equals(id?: Identifier<T>): boolean que verifica se um identificador é igual a outro através da comparação de seus valores.

Uma vez que estas classes estejam definidas voltamos a questão: Como gerar tais identificadores únicos? No construtor da classe Entity<T> é possível notar que o id é gerado através de uma factory de forma que a entidade não conheça detalhes de infraestrutura da aplicação.

Esta decisão foi tomada com base na análise de várias abordagens diferentes. Existe uma discussão muito grande na comunidade sobre de quem é a responsabilidade de gerar o id da entidade. Alguns defendem que o id deve ser gerado na camada de infraestrutura e ser repassado ao objeto de domínio.

Já outros escritores defendem que a geração do id deva ser feita na camada da aplicação, como é o caso dos Tenant Ids sugeridos por Vaughn Vernon em seu livro Implementing Domain-Driven-Design. Esta abordagem tem a vantagem de a geração do id não ser algo desconhecido da regra de negócio, ao mesmo tempo em que a regra de negócio não conhece detalhes de implementação da forma como esse id é gerado, ou seja, uma classe de caso de uso conhece o que deve ser feito para gerar o id, mas não conhece o como deve ser feito. A desvantagem, ao meu ver, é que sempre que uma classe na camada de casos de uso tiver que construir uma entidade, um repositório para geração do id deverá ser chamado. Isso leva a outro questionamento: Será que a geração do identificador da entidade não deveria ser responsabilidade da própria entidade?

Esta é uma abordagem defendida por grande parte da comunidade de desenvolvimento e foi a mesma abordagem que utilizei para gerar os ids de entidades. Para ser mais específico utilizei de uma técnica do livro Patterns of Enterprise Application Architecture de Martin Fowler, como é mostrado abaixo:

A classe UniqueEntityIdGeneratorFactory faz parte da camada de entidades, como mostrado anteriormente. Nesta classe utilizamos o padrão Singleton, de modo a garantir que essa classe será instanciada uma única vez para toda a aplicação.

A instância corrente da classe é recuperada através do método estático getInstance() . Com a instância recuperada é possível recuperar um gerador de id específico da entidade através do método getIdGeneratorFor(entity: Entity<any>): UniqueEntityIdGenerator .

A grande jogada desta classe é o método initizalize(factories: EntityIDFactories . Basicamente este método inicializa a instância da classe com todas os geradores de id de entidades existentes na aplicação. Este método será chamado uma única vez na hora de inicializar o serviço da aplicação, como será mostrado nos próximos artigos. Assim, a entidade não conhece detalhes de infraestrutura, porém a camada de infraestrutura conhece os detalhes da entidade, o que é aceitável quando falamos de arquitetura limpa.

Para garantir essa inversão de dependência entre a entidade e a camada de infraestrutura, a camada de entidades fornece a interface UniqueEntityIdGenerator , detalhada logo abaixo.

Essa interface será implementada na camada de infraestrutura tendo seus detalhes de implementação totalmente escondidos da camada de entidade. Assim, pouco importa para o domínio da aplicação se o ID da entidade é gerado através de uma biblioteca que fornece UUIDs ou através do incremento do último id persistido em uma tabela de um BD relacional. Tudo que a entidade precisa e irá saber é que existe algum gerador de id implementado na camada de infraestrutura que respeita o contrato da interface UniqueEntityIdGenerator .

Uma vez definidas todas as classes abstratas e templates da camada de entidades, podemos começar a definir as entidades concretas. Abaixo é mostrada a entidade Order que representa um pedido da nossa loja online:

Para criação da Order é utilizado o padrão static factory method em conjunto com um construtor privado. Assim garantimos que uma Order não será instanciada sem antes passar por validações de algumas pré-condições.

Caso o método build invalide a criação do pedido, a classe irá lançar um erro de domínio específico da classe Order, OrderError. Caso contrário a classe irá retornar um novo objeto do tipo Order.

Outro detalhe a se observar na classe Order é que esta classe tem, também, a responsabilidade de orquestrar o ciclo de vida (recuperação, criação, edição e exclusão) da classe LineItem já que existe um relacionamento de composição entre ambas.

O value object Invoice, que representa a fatura de um pedido, é definido de forma bem simples através de uma interface dentro do mesmo arquivo onde foi implementada a classe Order . Por ser um componente simples e que está no escopo do agregado de Order , não há necessidade de separá-los.

Por fim, temos a classe que representa o value object Address que, por ser mais complexa, exige um componente próprio, como é mostrado a seguir:

Por se tratar de um value object, a classe Address não recebe e nem gera um identificador único. Além disso, a igualdade entre dois objetos do tipo Address é garantida através da comparação entre todas as propriedades dos dois objetos, como mostrado anteriormente.

As classes das entidades Product , lineIteme Customer do nosso exemplo não serão detalhadas neste artigo por ter uma estrutura bem similar a classe Order . O objetivo principal deste artigo é mostrar como são implementados os tipos principais de classes do domínio da aplicação e como eles se relacionam. No entanto, se for de seu interesse, você pode conferir todas as classes do nosso exemplo neste repositório do GitHub.

Conclusão

Termino este artigo dizendo que tudo que foi escrito aqui são meros experimentos e conhecimentos compartilhados. Nenhum destes códigos são balas de prata e cada cenário específico exige uma solução específica. Porém, espero que este artigo lhe seja útil para definir o domínio da sua aplicação de forma simples e clara.

O próximo artigo da série Arquitetura Limpa e Typescript irá focar na definição e implementação da camada de Casos de Uso. See ya, folks :)

Referências:

--

--

Filipe Mata

Software developer and system architect. Also gamer and designer on free time. Always learning ;)