Skip to content

Characterization

Leo Fernandes edited this page Nov 14, 2022 · 13 revisions

A caracterização (ou definição) cuidadosa das classes é uma atividade chave quando se faz um Projeto Dirigido por Domínio (DDD). Felizmente, na maioria das vezes, é bastante óbvio a que categoria (ou divisão) uma determinada classe pertence. Outras vezes não é tão fácil de classificar e, então, é necessária uma análise mais cuidadosa, em colaboração com a equipe e possíveis refatorações.

Os mais difíceis de classificar são tipicamente Entidades (Entities), Agregados (Aggregates), Objetos de Valor (Value Objects) e Eventos de Domínio (Domain Events). Quando possível, você deve favorecer Objetos de Valor em detrimento de Entidades ou Eventos de Domínio, pois eles requerem menos atenção durante a implementação. Objetos de Valor podem ser criados e descartados à vontade, e como eles são imutáveis, podemos passá-los (como parâmetro, por exemplo) como quisermos. Devemos ser mais cuidadosos com as Entidades, pois a identidade e o ciclo de vida têm que ser cuidadosamente gerenciados.

Abaixo está uma breve passagem pelas classes chave da aplicação e a motivação por trás de sua escolha de implementação.

Entidades (Entities)

Cargo tem tanto uma identidade única clara quanto um ciclo de vida com transições de estado, portanto é uma entidade. Trata-se obviamente de um conceito chave no domínio. Muitas instâncias de Cargo existirão no sistema simultaneamente. As diferentes instâncias podem ter a mesma origem e destino, podem até conter os mesmos tipo de coisas (ou valores nos atributos), mas é importante para nós sermos capazes de rastrear instâncias individuais de carga. Em nosso caso, a identidade da carga é sua identificação de rastreamento. A identificação de rastreamento é atribuída no momento da criação e nunca é alterada. Este é o identificador usado para rastrear a localização da carga (por exemplo, a partir do site de rastreamento).

O estado da carga mudará durante sua vida útil. Seu status de transporte começará como NOT_RECEIVED, ou seja, reservado mas ainda não entregue à companhia de navegação no porto, e no caso normal terminará sua vida útil como CLAIMED (note que esta é uma propriedade da carga Delivery), rastreando o estado atual da carga. Durante a vida útil de uma carga ela pode receber novos destinos, seu itinerário pode ser alterado muitas vezes e sua entrega será recalculada à medida que novos HandlingEvents forem recebidos.

Voyage significa a viagem de uma embarcação da origem ao destino, normalmente composta de vários segmentos (CarrierMovements). Na aplicação, uma Voyage consiste de uma Schedule com os diferentes CarrierMovements nela e tem uma noção muito clara de identidade, VoyageNumber. Esta identificação pode ser algo como um número de vôo para embarques aéreos ou um número de viagem de navio, lembrando que isso não é o nome ou a identificação do navio real.

Sem surpresas, entidades de domínio são geralmente implementadas como entidades de Jakarta Persistence. Observe, entretanto, que às vezes é necessário ou conveniente implementar objetos de valor como entidades de Jakarta persistence também.

Value Objects (Objetos de Valor)

Uma Leg consiste em um ponto de partida e um ponto de chegada (de Location e para Location), e equivale a uma "perna" ou um trajeto de uma viagem. Uma perna não tem sentido de identidade; duas pernas com o mesmo localização inicial, localização final e viagem, pelo nosso modelo, são completamente intercambiáveis (sem identificação única). Como resultado, implementamos a perna como um Objeto de Valor imutável.

Um Itinerary consiste de uma lista de legs, com o local de carga (load) da primeira Leg da lista como ponto de partida do itinerário e o local de descarga (unload) do última Leg como destino final. O mesmo raciocínio que se aplica a Itinerary bem como a Leg, eles não têm identidade e são implementados como Objetos de Valor. Agora, uma carga pode certamente ter seu itinerário atualizado. Uma maneira de realizar isto seria manter a instância do itinerário original e atualizar as pernas/trajetos na lista do itinerário, neste caso o itinerário deve ser mutável e deve ser implementado como uma entidade. Com o itinerário como objeto de valor, como no caso de nosso modelo e implementação, atualizá-lo é, na verdade, uma operação para criar um novo itinerário completo através do RoutingService, substituindo o antigo intinerário. A implementação do gerenciamento do itinerários de uma carga é simplificada, mantendo o itinerário como um Objeto de Valor.

Objetos de valor são tipicamente implementados como objetos embutidos de Jakarta Persistence (Jakarta Persistence Embedded Object). Entretanto, às vezes é útil e válido implementá-los como entidades de Jakarta Persistence.

Domain Events (Eventos de Domínio)

Alumas coisas claramente possuem identidade, mas não possuem ciclo de vida, ou então possuem um ciclo-de-vida bem limitado com apenas um estado. Nós chamamos estas coisas de Eventos de Domínio e eles podem ser vistos como um híbrido entre Entidades e Objetos de Valor. Em nossa aplicação HandlingEvent é um evento de Domínioque representa um evento da vida real como um carregamento (load) ou descarregamento (unload) de uma carga, desembaraço aduaneiro (alfandega) e etc. Um HandlingEvent têm uma data/hora de conclusão, uma data/hora de registro. A data/hora de conclusão é a data em que o evento ocorreu e a data/hora de registro é a data em que o evento foi recebido pelo sistema. O ID do evento é composto da carga (Cargo), viagem (Voyage), data de conclusão, local e tipo (LOAD, UNLOAD etc).

Eventos de Domínio são usualmente implementadas como entidades Jakarta Persistence.

Aggregates (Agregados)

Na vida real, a maioria das coisas estão conectadas, direta ou indiretamente. Simular esta abordagem ao construir grandes sistemas de software tende a trazer complexidade desnecessária e mau desempenho. O DDD fornece táticas para ajudá-lo a resolver estas coisas, sendo os agregados um dos mais importantes. Os agregados ajudam a desacoplar grandes estruturas, estabelecendo regras para as relações entre as entidades. Um agregado é essencialmente um conjunto muito próximo de entidades e objetos de valor. Uma agregado raiz (aggregate root) é um tipo especial de entidade no agregado que controla o acesso externo ao conjunto de entidades e objetos de valor intimamente relacionados.

A classe Cargo é o agregado central na aplicação. As classes no agregado da carga estão no pacote org.eclipse.cargotracker.domain.model.cargo. A classe Cargo é a raiz do agregado, que contém tambem os Objetos de Valor, Delivery, Intinerary, Leg e mais algumas classes.

Handling é outro agregado importante. Ele contém os handling events que são registrados ao longo do progresso de uma carga, desde o NOT_ RECEIVED até o CLAIMED. Os handling events têm uma relação com a carga à qual o evento pertence. Isto é permitido, uma vez que a própria carga é uma raiz agregada.

A principal razão para não tornar o handling event parte do agregado da carga é o desempenho. Eventos de movimentação são recebidos de sistemas externos, por exemplo, sistemas de gerenciamento de armazém e sistemas de movimentação portuária, que chamam nosso HandlingReportService serviço REST. O número de eventos pode ser muito alto e é importante que nosso serviço web possa despachar as chamadas remotas rapidamente. Para poder suportar este caso de uso, precisamos lidar com as chamadas do serviço web de forma assíncrona, ou seja, não queremos carregar a grande estrutura de carga de forma síncrona para cada evento de manuseio recebido. Como todos os relacionamentos em um agregado devem ser tratados de forma síncrona, colocamos o handling event em um agregado próprio e somos capazes de processar os eventos rapidamente e, ao mesmo tempo, eliminar a locks no sistema.

Repositories (Repositórios)

Com os agregados e suas raízes identificadas, é trivial identificar os Repositórios. Os Repositórios recuperam e salvam as raízes agregadas de e para armazenamentos persistentes (em geral Bancos de Dados). Em nossa aplicação há um Repositório por raiz agregada. Por exemplo, o CargoRepository é responsável por encontrar e armazenar os agregados de carga. Os buscadores retornam instâncias de Carga ou listas de instâncias de Carga.

Enquanto as interfaces Repository fazem parte da camada de domínio, suas implementações fazem parte da camada de infraestrutura. Por exemplo, o CargoRepository tem uma implementação de Jakarta Persistence na camada de infraestrutura, JpaCargoRepository.

Os repositórios em Jakarta EE são tipicamente implementados usando Jakarta Persistence e CDI. O módulo DeltaSpike Data é particularmente útil na implementação de repositórios.

Factories (Fábricas)

Em alguns casos, criar entidades não é tão trivial como simplesmente chamar o operador new. Nesses casos, você vai querer encapsular a criação de entidades usando Fábricas. Em nossa aplicação, HandlingEventFactory é usado para criar handling events.

As fábricas são tipicamente implementadas usando CDI.

Domain Services (Serviços de Domínio)

Os serviços de domínio encapsulam conceitos-chave de domínio que simplesmente não são naturalmente modelados como coisas. Entretanto, os argumentos do método de serviços de domínio e os valores de retorno são geralmente classes de domínio (entidades, objetos de valor, etc). Às vezes, apenas a interface de serviço (o que o serviço faz) faz parte da camada de domínio, mas a implementação (como o serviço é feito) faz parte da camada de infraestrutura. Isto é análogo a como as interfaces de repositório são parte da camada de domínio, mas as implementações JPA não são.

Um bom exemplo disso é o RoutingService, que fornece acesso ao sistema de roteamento e é usado para encontrar possíveis rotas para uma determinada especificação. A implementação, ExternalRoutingService, se comunica com outro sistema e se traduz para/de um modelo API/dados externos na camada de infraestrutura.

.

Por outro lado, se o serviço puder ser implementado utilizando estritamente a camada de domínio, tanto a interface quanto a implementação poderiam fazer parte da camada de domínio.

Application Services (Serviços de Aplicação)

Os serviços de aplicação representam as operações comerciais de alto nível para o sistema e constituem a camada de aplicação. Eles fornecem uma abstração de alto nível para os clientes utilizarem quando interagirem com o domínio. O pacote org.eclipse.cargotracker.application contém todos os serviços para a aplicação, tais como BookingService e HandlingEventService. Os serviços da aplicação são um lugar natural para aplicar preocupações (concerns) tais como pooling, transações e segurança. É por isso que os serviços de aplicação são tipicamente implementados utilizando Enterprise Beans ou CDI Beans Transacionais.

Em algumas situações, como por exemplo, ao lidar com grafos de objetos de domínio lazy-load ou ao passar os valores de retorno dos serviços pelas fronteiras da rede, os serviços são envoltos em fachadas. As fachadas tratam de questões de gerenciamento de sessões ORM e/ou convertem os objetos de domínio em objetos de transferência de dados (DTO) mais portáteis que podem ser adaptados a casos específicos de uso. Nesse caso, consideramos a fachada de serialização DTO como parte da camada de interfaces. Veja BookingServiceFacade para um exemplo.