Як ставитися до валідації посилань між агрегатами?


11

Я трохи борюся з посиланням між агрегатами. Припустимо, що сукупність Carмає посилання на сукупність Driver. Ця посилання буде змодельована за наявності Car.driverId.

Тепер моя проблема полягає в тому, як далеко я повинен пройти перевірку створення Carсукупності в CarFactory. Чи слід вірити, що передане DriverIdпосилається на існуюче Driver чи слід перевірити інваріантність?

Для перевірки я бачу дві можливості:

  • Я міг би змінити підпис автомобільного заводу, щоб прийняти повну сутність водія. Тоді фабрика просто вибере ідентифікатор у цієї організації і з цим побудує автомобіль. Тут інваріант перевіряється неявно.
  • Я міг би мати посилання з DriverRepositoryв CarFactoryі явному виклику driverRepository.exists(driverId).

Але тепер мені цікаво, чи не занадто інваріантна перевірка? Я міг би уявити, що ці агрегати можуть жити в окремому обмеженому контексті, і тепер я забруднив автомобіль BC залежними від DriverRepository або Driver сутності водія BC.

Крім того, якщо я б поговорив з експертами по домену, вони ніколи не ставлять під сумнів справедливість таких посилань. Я відчуваю, що я забруднюю свою модель домену незв'язаними проблемами. Але знову ж таки, в якийсь момент введення користувача повинно бути підтверджено.

Відповіді:


6

Я міг би змінити підпис автомобільного заводу, щоб прийняти повну сутність водія. Тоді фабрика просто вибере ідентифікатор у цієї організації і з цим побудує автомобіль. Тут інваріант перевіряється неявно.

Цей підхід привабливий, оскільки ви отримуєте чек безкоштовно, і він добре узгоджується з всюдисущою мовою. A Carрухається не a driverId, а a 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.

Ні, ви насправді не хочете. Завжди є антикорупційний шар, щоб переконатися, що поняття одного контексту не перетікають в інший. Насправді набагато простіше, якщо ти маєш БЦ, присвячену асоціаціям водіїв автомобілів, тому що ти можеш моделювати існуючі концепції, такі як Carі Driverспеціально для цього контексту.

Тому ви можете мати DriverLookupServiceвизначене в БК відповідальне за управління об'єднаннями водіїв автомобілів. Ця служба може викликати веб-сервіс, що піддається впливу контексту управління драйверами, який повертає Driverекземпляри, які, швидше за все, будуть об'єктами цінності в цьому контексті.

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

Я не можу реально сказати вам, який підхід кращий для вашого домену, але, сподіваємось, це дасть вам достатньо розуміння для прийняття рішення.


3

Те, як ви ставите запитання (і пропонуєте дві альтернативи), начебто хвилює лише те, що водійський ідентифікатор все ще дійсний під час створення машини.

Однак ви також повинні бути стурбовані тим, що драйвер, пов’язаний з driverId, не видаляється до того, як машину буде видалено або надано інший драйвер (і, можливо, також, що драйвер не призначений для іншого автомобіля (це, якщо домен обмежує водія лише бути пов'язаним з одним автомобілем)).

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

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

Ви можете надати схему розподілу (наприклад, булеву чи довідкову кількість) в межах ВС сукупностей, що виділяються, або в рамках окремої БЦ, скажімо, тієї, яка відповідає за зв'язок між автомобілем та водієм. Якщо ви зробите перше, ви можете дозволити (дійсні) операції видалення, видані автомобілю або водію BC; якщо ви зробите останнє, вам потрібно буде запобігти видаленню з автомобіля та водія BC та замість цього надіслати їх через планувальник асоціації автомобілів та водіїв.

Ви також можете розділити деякі обов'язки щодо розподілу між БК наступним чином. Кожен з автомобілів і водіїв BC надає схему "розподілу", яка підтверджує та встановлює виділений булевий з цим BC; коли встановлено булевий розподіл, BC запобігає видаленню відповідних сутностей. (І система налаштована так, що автомобіль та водій BC дозволяють розподіляти та розставляти місця тільки з планування автомобільної / водійської асоціації.

Планування автомобіля та водія BC тоді веде календар водіїв, пов'язаних з автомобілем, на певні періоди часу / тривалість, теперішнє та майбутнє, і повідомляє інші БЦ про переїзд лише про останнє використання запланованого автомобіля чи водія.


Як більш радикальне рішення, ви можете ставитися до автомобілів та водіїв BC як до придатних лише історико-заводських заводів, залишаючи право власності на планувальник асоціацій автомобілів / водіїв. Автомобіль BC може генерувати новий автомобіль у комплекті з усіма деталями автомобіля, а також його VIN. Право власності на автомобіль здійснює планувальник асоціації автомобілів / водіїв. Навіть якщо асоціація автомобіля / водія видалена, а сама машина знищена, записи автомобіля все ще існують у БК автомобіля за визначенням, і ми можемо використовувати автомобіль BC для пошуку історичних даних; в той час як асоціації / власність автомобілів / водіїв (минуле, теперішнє та потенційно майбутні заплановані) здійснює інший БК.


2

Припустимо, що автомобіль-агрегат має посилання на драйвер агрегату. Ця посилання буде змодельована за допомогою Car.driverId.

Так, це правильний спосіб з'єднання одного агрегату з іншим.

якби я поговорив з експертами по домену, вони ніколи не ставлять під сумнів справедливість таких посилань

Не зовсім правильне запитання, щоб задати своїм доменним експертам. Спробуйте "яка вартість бізнесу, якщо драйвера не існує?"

Можливо, я б не використовував DriverRepository для перевірки драйвера. Натомість я використовую службу домену, щоб зробити це. Я думаю, що це робить кращу роботу щодо вираження намірів - під прикриттями служба домену все ще перевіряє систему запису.

Так щось на кшталт

class DriverService {
    private final DriverRepository driverRepository;

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

Ви фактично запитуєте домен про driverId у кількох різних точках

  • Від клієнта, перш ніж надсилати команду
  • У додатку перед тим, як передати команду моделі
  • В рамках доменної моделі під час обробки команд

Будь-яка або всі ці перевірки можуть зменшити помилки у введенні користувача. Але всі вони працюють із несвіжих даних; інший агрегат може змінитися відразу після того, як ми поставимо запитання. Тому завжди існує деяка небезпека помилкових негативів / позитивів.

  • У звіті про винятки запустіть після завершення команди

Тут ви все ще працюєте з несвіжими даними (агрегати можуть виконувати команди під час запуску звіту, можливо, ви не зможете побачити останні записи у всіх агрегатах). Але перевірки між агрегатами ніколи не будуть ідеальними (Car.create (драйвер: 7) працює одночасно з Driver.delete (драйвер: 7)) Отже, це дає вам додатковий рівень захисту від ризику.


1
Driver.deleteне повинно існувати. Я ніколи не бачив домену, де агрегати знищуються. Зберігаючи АР навколо себе, ти ніколи не можеш стати сирітами.
plalx

1

Це може допомогти запитати: Ви впевнені, що машини побудовані з водіями? Я ніколи не чув про машину, складену з водія у реальному світі. Причина, чому це питання є важливим, полягає в тому, що воно може вказувати вам у напрямку самостійного створення автомобілів та водіїв, а потім створювати якийсь зовнішній механізм, який призначає водія до автомобіля. Автомобіль може існувати без довідки водія і все ще залишатися дійсним автомобілем.

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


Я також подумав про взаємовідносини автомобіля / водія - але введення агрегату DriverAssignment просто рухається, посилання на яке потрібно підтвердити.
VoiceOfUnreason

1

Але тепер мені цікаво, чи не занадто інваріантна перевірка?

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

Тоді дизайн класу також робить непотрібним

  • Якщо є вимога "припаркований автомобіль може мати або не мати водія"
  • Якщо об'єкт Driver вимагає а DriverIdта встановлений у конструкторі.
  • Якщо Carпотрібні лише ті DriverId, майте Driver.Idгеттер. Без сетера.

Репозиторій не є місцем для правил бізнесу

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

Відокремлення проблем з порушенням

... буває, коли Repository.DriverIdExists()задають питання.

Побудувати доменний об’єкт. Якщо ні, Driverто, можливо, DriverInfo(просто, DriverIdі Name, скажімо,) об'єкт. Це 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дійсності - порушення єдиного принципу відповідальності; і зайвий код, ймовірно.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.