Quando uma Raiz Agregada deve conter outro AR (e quando não deve)

15

Deixe-me começar pedindo desculpas pela duração do post, mas eu realmente queria transmitir o máximo de detalhes antecipadamente, para que eu não dedique seu tempo indo e voltando nos comentários.

Estou projetando um aplicativo seguindo uma abordagem DDD e estou pensando em quais orientações posso seguir para determinar se uma Raiz Agregada deve conter outro AR ou se eles devem ser deixados como ARs independentes "independentes".

Tomemos o caso de um aplicativo de Relógio de ponto simples que permite que os funcionários entrem ou saiam do dia. A interface do usuário permite que eles insiram seu ID e PIN do funcionário, que são validados e o estado atual do funcionário recuperado. Se o funcionário estiver com entrada no relógio, a interface do usuário exibirá um botão "Clock Out"; e, por outro lado, se eles não tiverem entrada no relógio, o botão exibirá "Entrada do relógio". A ação realizada pelo botão também corresponde ao estado do funcionário.

O aplicativo é um cliente da Web que chama um servidor backend exposto por meio de uma interface de serviço RESTful. Minha primeira passagem na criação de URLs intuitivos e legíveis resultou nos dois seguintes pontos de extremidade:

http://myhost/employees/{id}/clockin
http://myhost/employees/{id}/clockout

NOTA: São utilizados após a validação do ID e PIN do funcionário e um "token" representando o "usuário" é passado em um cabeçalho. Isso ocorre porque existe um "modo de gerente" que permite que um gerente ou supervisor faça o registro de entrada ou saída de outro funcionário. Mas, para o bem desta discussão, estou tentando manter as coisas simples.

No servidor, eu tenho um ApplicationService que fornece a API. Minha idéia inicial para o método ClockIn é algo como:

public void ClockIn(String id)
{
    var employee = EmployeeRepository.FindById(id);

    if (employee == null) throw SomeException();

    employee.ClockIn();

    EmployeeRepository.Save();
}

Isso parece bastante direto até percebermos que as informações do cartão de ponto do Funcionário são realmente mantidas como uma lista de transações. Isso significa que toda vez que eu chamo ClockIn ou ClockOut, não estou mudando diretamente o estado do Employee, mas adicionando uma nova entrada no TimeSheet do Employee. O estado atual do Empregado (com ou sem clock) é derivado da entrada mais recente no TimeSheet.

Portanto, se eu seguir o código mostrado acima, meu repositório precisará reconhecer que as propriedades persistentes do Employee não foram alteradas, mas que uma nova entrada foi adicionada ao TimeSheet do Employee e execute uma inserção no armazenamento de dados.

Por outro lado (e aqui está a pergunta final da publicação), o TimeSheet parece ser uma raiz agregada e possui identidade (o ID e o período do funcionário) e eu poderia implementar facilmente a mesma lógica do TimeSheet.ClockIn (ID do Empregado).

Encontro-me debatendo os méritos das duas abordagens e, como afirmado no parágrafo inicial, fico imaginando quais critérios devo avaliar para determinar qual abordagem é mais adequada para o problema.

SonOfPirate
fonte
Eu editei / atualizei a postagem para que a pergunta fique mais clara e use um cenário melhor (espero).
SonOfPirate

Respostas:

4

Eu estaria inclinado a implementar um serviço para rastreamento de tempo:

public interface ITimeSheetTrackingService
{
   void TrackClockIn(Employee employee, Timesheet timesheet);

   void TrackClockOut(Employee employee, Timesheet timesheet);

}

Eu não acho que é responsabilidade do time-time entrar ou sair, nem é responsabilidade do funcionário.

CodeART
fonte
3

As raízes agregadas não devem se conter (embora possam conter IDs para outras pessoas).

Primeiro, o TimeSheet é realmente uma raiz agregada? O fato de ter identidade faz dela uma entidade, não necessariamente uma AR. Confira uma das regras:

Root Entities have global identity.  Entities inside the boundary have local 
identity, unique only within the Aggregate.

Você define a identidade do TimeSheet como ID do funcionário e período, o que sugere que o TimeSheet faz parte do Employee, sendo o período a identidade local. Como esse é um aplicativo de Relógio de ponto , o principal objetivo do Employee é ser um contêiner para o TimeSheets?

Supondo que você precise de ARs, ClockIn e ClockOut parecem mais operações do TimeSheet do que de funcionários. Seu método da camada de serviço pode ser algo como isto:

public void ClockIn(String employeeId)
{
    var timeSheet = TimeSheetRepository.FindByEmployeeAndTime(employeeId, DateTime.Now);    
    timeSheet.ClockIn();    
    TimeSheetRepository.Save();
}

Se você realmente precisar rastrear o estado de registro de hora no Employee e no TimeSheet, consulte os Eventos do Domínio (não acredito que estejam no livro de Evans, mas existem inúmeros artigos on-line). Seria algo como: Employee.ClockIn () gera o evento EmployeeClockedIn, que um manipulador de eventos pega e, por sua vez, chama TimeSheet.LogEmployeeClockIn ().

Misko
fonte
Eu vejo seus pontos. No entanto, existem certas regras que restringem se e quando um Funcionário pode fazer o check-in. Essas regras são principalmente orientadas pelo estado atual do Funcionário, como se elas foram rescindidas, já com entrada no relógio, agendadas para trabalhar naquele dia ou até mesmo o mudança de corrente etc. O TimeSheet deve possuir esse conhecimento?
SonOfPirate
3

Estou projetando um aplicativo seguindo uma abordagem DDD e estou pensando em quais orientações posso seguir para determinar se uma Raiz Agregada deve conter outro AR ou se eles devem ser deixados como ARs independentes "independentes".

Uma raiz agregada nunca deve conter outra raiz agregada.

Na sua descrição, você tem uma entidade Funcionário e da mesma forma uma entidade do Quadro de Horários. Essas duas entidades são distintas, mas podem incluir referências uma à outra (por exemplo: este é o quadro de horários de Bob).

Isso é modelagem básica.

A questão raiz agregada é um pouco diferente. Se essas duas entidades são transacionalmente distintas uma da outra, elas podem ser modeladas corretamente como dois agregados distintos. Por distinção de transação, quero dizer que não há invariável nos negócios que exija conhecer o estado atual de Employee e o estado atual de TimeSheet simultaneamente.

Com base no que você descreve, Timesheet.clockIn e Timesheet.clockOut nunca precisam verificar dados no Employee para determinar se o comando é permitido. Portanto, para esse grande problema, dois RA diferentes parecem razoáveis.

Outra maneira de considerar os limites da RA seria perguntar que tipos de edições podem acontecer simultaneamente. O gerente está autorizado a marcar o empregado enquanto o RH está brincando com o perfil do funcionário?

Por outro lado (e aqui está a questão final da publicação), o TimeSheet parece ser uma raiz agregada e possui identidade (o ID do funcionário e o período)

Identidade significa apenas que é uma entidade - é a invariância dos negócios que determina se deve ser considerada uma raiz agregada separada.

No entanto, existem certas regras que restringem se e quando um Funcionário pode fazer o registro de entrada. Essas regras são principalmente orientadas pelo estado atual do Funcionário, como se elas foram rescindidas, já registradas, programadas para trabalhar naquele dia ou até mesmo o mudança de corrente, etc. O TimeSheet deve possuir esse conhecimento?

Talvez - o agregado deveria. Isso não significa necessariamente que o Quadro de Horários deva.

Ou seja, se as regras para modificar um quadro de horários dependem do estado atual da entidade Employee, Employee e Timesheet definitivamente precisam fazer parte do mesmo agregado, e o agregado como um todo é responsável por garantir que as regras sejam seguido.

Um agregado possui uma entidade raiz; identificá-lo faz parte do quebra-cabeça. Se um funcionário tiver mais de um quadro de horários e ambos fizerem parte do mesmo agregado, o quadro de horários definitivamente não será a raiz. O que significa que o aplicativo não pode modificar ou despachar diretamente os comandos para o quadro de horários - eles precisam ser despachados para o objeto raiz (presumivelmente o Funcionário), que pode delegar parte da responsabilidade.

Outra verificação seria considerar como os quadros de horários são criados. Se eles são criados implicitamente quando um funcionário entra, então essa é outra dica de que eles são uma entidade subordinada no agregado.

Como um aparte, é improvável que seus agregados tenham seu próprio senso de tempo. Em vez disso, o tempo deve ser passado para eles

employee.clockIn(when);
VoiceOfUnreason
fonte