Как лечить валидацию ссылок между агрегатами?

11

Я немного борюсь со ссылками между агрегатами. Давайте предположим, что агрегат Carимеет ссылку на агрегат Driver. Эта ссылка будет смоделирована с помощью Car.driverId.

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

Для проверки я вижу две возможности:

  • Я мог бы изменить подпись автомобильного завода, чтобы принять полное лицо водителя. Затем фабрика просто выберет идентификатор у этой сущности и построит машину с этим. Здесь инвариант проверяется неявно.
  • Я мог бы иметь ссылку на DriverRepositoryв CarFactoryи явно вызов driverRepository.exists(driverId).

Но теперь мне интересно, не слишком ли много проверок инвариантов? Я мог предположить, что эти агрегаты могли бы жить в отдельном ограниченном контексте, и теперь я бы загрязнил автомобиль BC зависимостями от DriverRepository или объекта Driver драйвера BC.

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

Маркус Малуш
источник

Ответы:

6

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

Этот подход привлекателен, так как вы получаете чек бесплатно, и он хорошо согласуется с вездесущим языком. А Carдвижет не а driverId, а Driver.

Этот подход фактически используется Воном Верноном в его ограниченном контексте Identity & Access, где он передает Userагрегат в Groupагрегат, но Groupединственное, что относится к типу значения GroupMember. Как вы можете видеть, это также позволяет ему проверять включение пользователя (мы хорошо знаем, что проверка может быть устаревшей).

    public void addUser(User aUser) {
        //original code omitted
        this.assertArgumentTrue(aUser.isEnabled(), "User is not enabled.");

        if (this.groupMembers().add(aUser.toGroupMember()) && !this.isInternalGroup()) {
            //original code omitted
        }
    }

Однако, передавая Driverэкземпляр, вы также открываете себя для случайного изменения Driverвнутри Car. Передавая значение, вы сможете легче рассуждать об изменениях с точки зрения программиста, но в то же время DDD - это повсеместный язык, так что, возможно, он стоит риска.

Если вы действительно можете придумать хорошие имена для применения принципа сегрегации интерфейса (ISP), тогда вы можете положиться на интерфейс, который не имеет поведенческих методов. Возможно, вы могли бы также придумать концепцию объекта-значения, которая представляет собой неизменяемую ссылку на драйвер и может быть создана только из существующего драйвера (например DriverDescriptor driver = driver.descriptor()).

Я мог предположить, что эти агрегаты могли бы жить в отдельном ограниченном контексте, и теперь я бы загрязнил автомобиль BC зависимостями от DriverRepository или объекта Driver драйвера BC.

Нет, на самом деле ты бы не стал. Всегда есть антикоррупционный слой, чтобы убедиться, что концепции одного контекста не перетекут в другой. На самом деле гораздо проще, если у вас есть BC, предназначенный для ассоциаций водителей, потому что вы можете моделировать существующие концепции, такие как Carи Driverспециально для этого контекста.

Таким образом, DriverLookupServiceв BC вы можете назначить ответственного за управление ассоциациями водителей автомобилей. Эта служба может вызывать веб-службу, предоставляемую контекстом управления драйверами, которая возвращает Driverэкземпляры, которые, скорее всего, будут объектами значения в этом контексте.

Обратите внимание, что веб-сервисы не обязательно являются лучшим методом интеграции между BC. Вы также можете полагаться на обмен сообщениями, когда, например, UserCreatedсообщение из контекста управления драйверами будет использоваться в удаленном контексте, который будет хранить представление драйвера в своей собственной БД. Затем он DriverLookupServiceможет использовать эту БД, и данные драйвера будут обновляться с последующими сообщениями (например, DriverLicenceRevoked).

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

plalx
источник
3

То, как вы задаете вопрос (и предлагаете две альтернативы), заключается в том, что единственное беспокойство заключается в том, что driverId остается действительным на момент создания автомобиля.

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

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

Кстати, я согласен с @PriceJones, что связь между автомобилем и водителем, вероятно, является ответственностью, отдельной от автомобиля или водителя. Такая связь со временем будет только усложняться, потому что это звучит как проблема с расписанием (водители, машины, временные интервалы / окна, заменители и т. Д.). Даже если это больше похоже на проблему с регистрацией, может потребоваться историческая проблема. регистрации, а также текущие регистрации. Таким образом, он вполне может заслужить свою собственную БК.

Вы можете предоставить схему распределения (например, логическое или ссылочное число) в пределах BC совокупных объектов, которые выделяются, или в пределах отдельного BC, скажем, того, который отвечает за установление связи между автомобилем и водителем. Если вы делаете первое, вы можете разрешить (действительные) операции удаления, выданные автомобилю или водителю BC; если вы сделаете последнее, вам нужно будет предотвратить удаление из BC автомобиля и водителя и вместо этого отправить их через планировщик ассоциации автомобиля и водителя.

Вы также можете разделить некоторые обязанности по распределению между BC следующим образом. Каждое BC автомобиля и водителя предоставляет схему «распределения», которая проверяет и устанавливает выделенное логическое значение с этим BC; когда установлено их логическое распределение, BC предотвращает удаление соответствующих объектов. (И система настроена таким образом, что BC автомобиля и водителя разрешает только распределение и освобождение из BC BC планирования движения).

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


Как более радикальное решение, вы можете рассматривать BC и Car & Driver как фабрики с историческими записями только для дополнения, оставляя право собственности на планировщик ассоциации Car / Driver. Автомобиль BC может генерировать новый автомобиль, в комплекте со всеми деталями автомобиля, а также его VIN. Право собственности на автомобиль обрабатывается планировщиком ассоциации автомобилей / водителей. Даже если связь между автомобилем и водителем удалена, а сам автомобиль уничтожен, записи об автомобиле все еще существуют в автомобиле BC по определению, и мы можем использовать автомобиль BC для поиска исторических данных; в то время как ассоциации / владения автомобиля / водителя (прошлые, настоящие и, возможно, запланированные на будущее) обрабатываются другим BC.

Эрик Эйдт
источник
2

Давайте предположим, что совокупный Автомобиль имеет ссылку на совокупного Водителя. Эта ссылка будет смоделирована с помощью Car.driverId.

Да, это правильный способ соединить один агрегат с другим.

если бы я поговорил с экспертами по предметной области, они бы никогда не подвергли сомнению обоснованность таких ссылок

Не совсем правильный вопрос, чтобы задать свой домен экспертов. Попробуйте "сколько стоит бизнес, если водителя не существует?"

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

Так что-то вроде

class DriverService {
    private final DriverRepository driverRepository;

    boolean doesDriverExist(DriverId driverId) {
        return driverRepository.exists(driverId);
    }
}

Вы фактически запрашиваете домен о идентификаторе драйвера в нескольких разных точках.

  • От клиента, перед отправкой команды
  • В приложении перед передачей команды модели
  • В рамках модели предметной области при обработке команд

Любая или все эти проверки могут уменьшить количество ошибок при вводе пользователем. Но все они работают от устаревших данных; другая совокупность может измениться сразу после того, как мы зададим вопрос. Таким образом, всегда есть некоторая опасность ложных негативов / позитивов.

  • В отчете об исключении, запустите после завершения команды

Здесь вы по-прежнему работаете с устаревшими данными (агрегаты могут выполнять команды во время выполнения отчета; возможно, вы не сможете увидеть самые последние записи во все агрегаты). Но проверки между агрегатами никогда не будут идеальными (Car.create (драйвер: 7) работает одновременно с Driver.delete (драйвер: 7)), так что это дает вам дополнительный уровень защиты от риска.

VoiceOfUnreason
источник
1
Driver.deleteне должно существовать Я никогда не видел домена, где агрегаты разрушаются. Держа AR вокруг, вы никогда не получите сирот.
plalx
1

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

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

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

Но теперь мне интересно, не слишком ли много проверок инвариантов?

Я думаю так. Выборка данного DriverId из БД возвращает пустой набор, если он не существует. Таким образом, проверка возвращаемого результата делает ненужным выяснение, существует ли он (и затем извлечение).

Тогда дизайн класса делает это ненужным

  • Если есть требование «на припаркованном автомобиле может быть водитель, а может и нет»
  • Если объект Driver требует DriverIdи задается в конструкторе.
  • Если Carнужно только DriverId, есть Driver.Idдобытчик. Нет сеттера.

Хранилище не место для бизнес-правил

  • A Carзаботится, если у него есть Driver(или, по крайней мере, его ID). А Driverзаботится, если у него есть DriverId. В Repositoryзаботится о целостности данных и не мог заботиться меньше о водителе меньше автомобилей.
  • БД будет иметь правила целостности данных. Ненулевые ключи, ненулевые ограничения и т. Д. Но целостность данных касается схемы данных / таблиц, а не бизнес-правил. У нас сильно коррелированные, симбиотические отношения в этом случае, но мы не смешиваем их.
  • Тот факт, что объект DriverIdявляется бизнес-доменом, обрабатывается в соответствующих классах.

Разделение проблем Нарушение

... происходит, когда Repository.DriverIdExists()задает вопрос.

Построить доменный объект. Если не Driverто , возможно DriverInfo(как раз DriverIdи Name, скажем) объект. Утверждено DriverIdпри строительстве. Он должен существовать и быть правильным типом, и все остальное. Тогда это проблема дизайна клиентского класса, как работать с несуществующим driver / driverId.

Может быть, Carхорошо без водителя, пока вы не позвоните Car.Drive(). В этом случае Carобъект, конечно, обеспечивает свое собственное состояние. Не могу ездить без Driver- ну, пока не совсем.

Отделять свойство от его класса плохо

Конечно, есть, Car.DriverIdесли хотите. Но это должно выглядеть примерно так:

public class Car {
    // Non-null driver has a driverId by definition/contract.
    protected DriverInfo myDriver;
    public DriverId {get { return myDriver.Id; }}

    public void Drive() {
       if (myDriver == null ) return errorMessage; // or something
       // ... continue driving
    }
}

Не это:

public class Car {
    public int DriverId {get; protected set;}
}

Теперь Carнужно разобраться со всеми вопросами DriverIdобоснованности - нарушение принципа единой ответственности; и избыточный код, вероятно.

radarbob
источник