Когда Совокупный Корень должен содержать другой AR (и когда это не должно)

15

Позвольте мне начать с того, что сначала извиняюсь за длину поста, но я действительно хотел передать как можно больше подробностей, чтобы не тратить время на комментарии в комментариях.

Я разрабатываю приложение, следуя подходу DDD, и мне интересно, каким руководством я могу следовать, чтобы определить, должен ли Совокупный корень содержать другой AR или они должны быть оставлены в качестве отдельных "автономных" AR.

Возьмите случай с простым приложением Time Clock, которое позволяет Сотрудникам самим входить или выходить в течение дня. Пользовательский интерфейс позволяет им вводить идентификатор сотрудника и PIN-код, который затем проверяется и восстанавливается текущее состояние сотрудника. Если сотрудник в данный момент работает, пользовательский интерфейс отображает кнопку «Clock Out»; и, наоборот, если они не синхронизированы, кнопка показывает «Clock In». Действие, выполняемое кнопкой, также соответствует состоянию работника.

Приложение представляет собой веб-клиент, который вызывает внутренний сервер, предоставляемый через интерфейс службы RESTful. Мой первый опыт создания интуитивно понятных и удобных для чтения URL привел к следующим двум конечным точкам:

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

ПРИМЕЧАНИЕ. Они используются после проверки идентификатора и PIN-кода сотрудника, а в заголовке передается «токен», представляющий «пользователя». Это связано с тем, что существует «режим менеджера», который позволяет менеджеру или руководителю включать или выключать другого сотрудника. Но ради этой дискуссии я стараюсь сделать это простым.

На сервере у меня есть ApplicationService, который предоставляет API. Моя первоначальная идея для метода ClockIn что-то вроде:

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

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

    employee.ClockIn();

    EmployeeRepository.Save();
}

Это выглядит довольно просто, пока мы не поймем, что информация о тайм-карте сотрудника фактически поддерживается в виде списка транзакций. Это означает, что каждый раз, когда я вызываю ClockIn или ClockOut, я не изменяю напрямую состояние Сотрудника, а вместо этого добавляю новую запись в Табель рабочего времени Сотрудника. Текущее состояние Сотрудника (синхронизировано или нет) основано на самой последней записи в Табеле рабочего времени.

Итак, если я воспользуюсь кодом, показанным выше, мой репозиторий должен признать, что постоянные свойства Сотрудника не изменились, но что новая запись была добавлена ​​в Табель рабочего времени Сотрудника и выполнить вставку в хранилище данных.

С другой стороны (и вот главный вопрос поста), TimeSheet выглядит так, как будто это Aggregate Root, так как у него есть идентичность (идентификатор сотрудника и период), и я мог бы так же легко реализовать ту же логику, что и TimeSheet.ClockIn. (EmployeeID).

Я обсуждаю достоинства двух подходов и, как указано во вступительном абзаце, задаюсь вопросом, какие критерии мне следует оценить, чтобы определить, какой подход больше подходит для этой проблемы.

SonOfPirate
источник
Я отредактировал / обновил пост, поэтому вопрос яснее и использует лучший сценарий (надеюсь).
SonOfPirate

Ответы:

4

Я был бы склонен реализовать сервис для учета времени:

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

   void TrackClockOut(Employee employee, Timesheet timesheet);

}

Я не думаю, что ответственность за включение или выключение часов лежит на расписании, а также на сотруднике.

CodeART
источник
3

Совокупные корни не должны содержать друг друга (хотя они могут содержать идентификаторы для других).

Во-первых, действительно ли TimeSheet является совокупным корнем? Тот факт, что он имеет идентичность, делает его сущностью, а не обязательно AR. Проверьте одно из правил:

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

Вы определяете личность TimeSheet как идентификатор сотрудника и период времени, что предполагает, что TimeSheet является частью Employee, а период времени является локальной идентификацией. Поскольку это приложение Time Clock , является ли главное назначение Employee контейнером для TimeSheets?

Предполагая, что у вас есть AR, ClockIn и ClockOut больше похожи на операции с расписанием, чем на сотрудников. Ваш метод уровня сервиса может выглядеть примерно так:

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

Если вам действительно нужно отслеживать синхронизированное состояние в Employee и TimeSheet, посмотрите на события домена (я не думаю, что они есть в книге Эванса, но в Интернете есть множество статей). Это будет выглядеть примерно так: Employee.ClockIn () вызывает событие EmployeeClockedIn, которое обрабатывает обработчик события и, в свою очередь, вызывает TimeSheet.LogEmployeeClockIn ().

Мисько
источник
Я вижу ваши очки. Тем не менее, существуют определенные правила, которые ограничивают возможность и время, когда сотрудник может быть синхронизирован. Эти правила в основном определяются текущим состоянием сотрудника, например, уволены ли они, уже работают, запланированы ли на этот день работы или даже текущий сдвиг и т. д. Должен ли табель рабочего времени обладать этими знаниями?
SonOfPirate
3

Я разрабатываю приложение, следуя подходу DDD, и мне интересно, каким руководством я могу следовать, чтобы определить, должен ли Совокупный корень содержать другой AR или они должны быть оставлены в качестве отдельных "автономных" AR.

Совокупный корень никогда не должен содержать другого совокупного корня.

В вашем описании у вас есть сущность Employee , а также сущность расписания. Эти две сущности различны, но могут включать ссылки друг на друга (например, это расписание Боба).

Это базовое моделирование.

Совокупный корневой вопрос немного отличается. Если эти два объекта транзакционно отличаются друг от друга, то их можно правильно смоделировать как два различных агрегата. Под транзакционным отличием я подразумеваю, что не существует бизнес-инварианта, который требовал бы одновременного знания текущего состояния Employee и текущего состояния TimeSheet.

Исходя из того, что вы описываете, Timesheet.clockIn и Timesheet.clockOut никогда не нужно проверять какие-либо данные в Employee, чтобы определить, разрешена ли команда. Так что для этой большой части проблемы два разных AR кажутся разумными.

Еще один способ рассмотрения границ AR - спросить, какие виды правок разрешены одновременно. Разрешено ли менеджеру отключать сотрудника от работы, в то время как HR возится с профилем сотрудника?

С другой стороны (и вот последний вопрос поста), табель рабочего времени выглядит так, как будто он является совокупным корнем, а также имеет идентичность (идентификатор сотрудника и период)

Идентичность только означает, что это сущность - это бизнес-инвариант, определяющий, следует ли считать его отдельным агрегированным корнем.

Тем не менее, существуют определенные правила, которые ограничивают, когда и когда сотрудник может быть синхронизирован. Эти правила в основном определяются текущим состоянием сотрудника, например, уволены ли они, уже синхронизированы, запланированы ли на работу в тот день или даже текущий сдвиг и т. д. Должен ли табель рабочего времени обладать этими знаниями?

Возможно - совокупность должна. Это не обязательно означает, что расписание должно.

То есть, если правила для изменения расписания зависят от текущего состояния сущности Employee, то Employee и Timesheet обязательно должны быть частью одного агрегата, а агрегат в целом отвечает за обеспечение того, чтобы правила последовало.

Агрегат имеет одну корневую сущность; выявление этого является частью головоломки. Если у сотрудника есть более одного расписания, и они оба являются частью одного и того же агрегата, то расписание точно не является корневым. Это означает, что приложение не может напрямую изменять или отправлять команды расписанию - они должны быть отправлены корневому объекту (предположительно Сотруднику), который может делегировать часть ответственности.

Другой проверкой было бы рассмотреть, как создаются табели. Если они создаются неявно при включении сотрудника, это еще один намек на то, что они являются подчиненным объектом в совокупности.

Кроме того, маловероятно, чтобы у ваших агрегатов было собственное чувство времени. Вместо этого время должно быть передано им

employee.clockIn(when);
VoiceOfUnreason
источник