Як саме слід перевірити команду CQRS та перетворити на об’єкт домену?


22

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

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

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

У типовому додатку DDD бізнес-логіка знаходиться в межах сутності, ціннісних об'єктів, сукупних коренів, вони багаті як даними, так і поведінкою. Але команда не є об'єктом домену, тому вона не повинна обмежуватися представленням даних домену, тому що це надто сильно напружує їх.

Тож справжнє питання: Де саме логіка?

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

Скажімо, агрегат Carвикористовує два компоненти:

  • Transmission,
  • Engine.

Обидва Transmissionі Engineцінні об'єкти представлені як супер типи і мають відповідно до підтипів, Automaticі Manualпередач, Petrolі Electricдвигунів відповідно.

У цій області живе на своєму власному і успішно створена Transmission, будь то Automaticчи Manual, або будь-який тип Engineцілком нормально. Але Carсукупність вводить кілька нових правил, застосовних лише тоді, коли Transmissionі Engineоб’єкти використовуються в одному контексті. А саме:

  • Коли автомобіль використовує Electricдвигун, єдиний дозволений тип передачі - це Automatic.
  • Коли автомобіль використовує Petrolдвигун, він може мати будь-який тип Transmission.

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

Один із варіантів - перенести цю перевірку ділової логіки на саму валідатор команд, але це, мабуть, не вірно. Схоже, я би деконструював команду, перевіряв її властивості, отримані за допомогою getters та порівнював їх у валідаторі та перевіряв результати. Це кричить як порушення закону Деметра для мене.

Відмовившись від згаданого варіанту перевірки, оскільки він не здається життєздатним, здається, що слід використовувати команду та побудувати з неї сукупність. Але де повинна існувати ця логіка? Чи повинен він знаходитися в обробниках команд, відповідальних за обробку конкретної команди? Або це може бути в межах валідатора команд (мені не подобається такий підхід)?

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


Давайте уявимо собі інший сценарій, що змішує різні процеси перевірки - створення нового користувача за допомогою CreateUserкоманди.

Команда містить Idкористувачів, які будуть створені та їх Email.

Система встановлює такі правила для електронної адреси користувача:

  • повинні бути унікальними,
  • не повинно бути порожнім,
  • має містити не більше 100 символів (максимальна довжина колони db).

У цьому випадку, хоча наявність унікальної електронної пошти є діловим правилом, перевірка її у сукупності має дуже мало сенсу, тому що мені потрібно буде завантажити весь набір поточних електронних листів у систему на пам'ять і перевірити електронну пошту в команді проти сукупності ( Eeeek! Щось, щось, виконання.). Через це я перенесу цю перевірку на валідатор команд, який буде вважатися UserRepositoryзалежністю та використовувати репозиторій, щоб перевірити, чи є вже користувач з електронною поштою, присутній у команді.

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

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

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


1 Працюючи розробником PHP, відповідальним за створення систем RESTful, моя інтерпретація CQRS трохи відхиляється від стандартного підходу до обробки асинхронно-командної системи , наприклад, іноді повертає результати команд через необхідність синхронної обробки команд.


мені потрібен приклад код я думаю. як виглядають ваші командні об’єкти і де ви їх створюєте?
Еван

@Ewan Я додаю зразки коду або пізніше сьогодні, або завтра. Виїжджаючи в подорож за кілька хвилин.
Енді

Будучи програмістом PHP, я пропоную поглянути на мою реалізацію CQRS + ES: github.com/xprt64/cqrs-es
Константин Гальбену

@ConstantinGALBENU Якщо ми вважаємо, що трактування CQRS Грега Янга є правильним (що ми, мабуть, повинні), то ваше розуміння CQRS неправильне - або принаймні ваша реалізація PHP. Команди не повинні оброблятися агрегатами безпосередньо. Командами слід обробляти обробники команд, які можуть призвести до зміни агрегатів, які потім створюють події, які будуть використані для реплікацій стану.
Енді

Я не думаю, що наші інтерпретації відрізняються. Вам просто доведеться більше копатися в DDD (на тактичному рівні агрегатів) або відкривати очі ширше. Існує щонайменше два стилі реалізації CQRS. Я використовую одну з них. Моя реалізація більше нагадує модель Actor і робить шар додатка дуже тонким, що завжди добре. Я помітив, що у цих службах додатків багато дублювання коду, і вирішив замінити їх на CommandDispatcher.
Костянтин Гальбену

Відповіді:


22

Наступна відповідь знаходиться в контексті стилю CQRS, який рекламує cqrs.nu, в якому команди надходять безпосередньо на сукупності. У цьому архітектурному стилі служби прикладних програм замінюються інфраструктурним компонентом ( CommandDispatcher ), який ідентифікує агрегат, завантажує його, відправляє команду та зберігає сукупність (як ряд подій, якщо використовується пошук подій).

Тож справжнє питання: Де саме логіка?

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

  • структура самого об'єкта команди; конструктор команди має деякі необхідні поля, які повинні бути присутніми для створення команди; це перша і найшвидша перевірка; це, очевидно, міститься в команді.
  • Перевірка поля низького рівня, як-от порожнеча деяких полів (наприклад, ім'я користувача) або формат (дійсна адреса електронної пошти). Цей вид перевірки повинен міститися всередині самої команди, у конструкторі. Існує інший стиль використання isValidметоду, але це здається мені безглуздим, оскільки комусь доведеться пам’ятати, щоб викликати цей метод, коли насправді достатньо успішної інстанції команди.
  • окремі command validatorsкласи, які відповідають за перевірку команди. Я використовую такий вид перевірки, коли мені потрібно перевірити інформацію з декількох агрегатів або зовнішніх джерел. Ви можете використовувати це для перевірки унікальності імені користувача. Command validatorsможе бути введено будь-які залежності, як сховища. Майте на увазі, що ця перевірка врешті-решт узгоджується із сукупністю (тобто, коли користувач створюється, тим часом може бути створений інший користувач з таким самим іменем)! Крім того, не намагайтеся вводити сюди логіку, яка повинна знаходитися всередині агрегату! Валідатори команд відрізняються від менеджерів Sagas / Process, які генерують команди на основі подій.
  • сукупні методи, які отримують та обробляють команди. Це остання (різновид) перевірки, яка має місце. Сукупність витягує дані з команди і використовуючи якусь основну бізнес-логіку, яку вона приймає (виконує зміни у своєму стані) або відхиляє їх. Ця логіка перевіряється чітко послідовно. Це остання лінія оборони. У вашому прикладі тут When a car uses Electric engine the only allowed transmission type is Automaticслід перевірити правило .

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

Використовуючи вищезазначені методи, ніхто не може створити недійсні команди або обійти логіку всередині агрегатів. Валідатори команд автоматично завантажуються + викликаються, CommandDispatcherтому ніхто не може відправити команду безпосередньо в сукупність. Можна було б викликати метод у сукупності, що передає команду, але не зміг зберегти зміни, тому це було б безглуздо / нешкідливо.

Працюючи розробником PHP, відповідальним за створення систем RESTful, моя інтерпретація CQRS трохи відхиляється від стандартного підходу асинхронно-командної обробки, наприклад, іноді повертає результати команд через необхідність синхронної обробки команд.

Я також PHP-програміст, і нічого не повертаю з моїх командних обробників (сукупні методи у формі handleSomeCommand). Я, однак, досить часто повертаю інформацію клієнту / браузеру в HTTP response, наприклад, ідентифікатор новоствореного агрегатного кореня або щось із моделі читання, але я ніколи не повертаю (дійсно ніколи ) нічого з моїх сукупних методів командування. Достатньо простого факту, що команда була прийнята (і оброблена - ми говоримо про синхронну обробку PHP, правда ?!).

Ми повертаємо щось до браузера (і все ще робимо CQRS за книгою), оскільки CQRS не є архітектурою високого рівня .

Приклад роботи валідаторів команд:

Шлях команди через валідатори команд на шляху до Агрегату


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

@ king-side-slide немає, якщо у вас є EmailAddressоб'єкт значення, який перевіряє себе.
Константин Гальбену

Це цілком правильно. Можна EmailAddressзменшити дублювання, щоб зменшити дублювання. Що ще важливіше, але, роблячи це, ви також перейдете логіку із своєї команди та у свій домен. Варто зазначити, що це можна зайняти занадто далеко. Часто подібні знання (об'єкти цінності) можуть мати різні вимоги до перевірки залежно від того, хто їх використовує. EmailAddressє зручним прикладом, оскільки вся концепція цього значення має глобальні вимоги перевірки.
король-слайд-слайд

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

1
@ king-side-slide Конкретний приклад є UserCanPlaceOrdersOnlyIfHeIsNotLockedValidator. Ви можете бачити, що це окремий домен, який належить до Замовлень, тому його не можна перевірити самим OrderAggregate.
Константин Гальбену

6

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

Система CQRS (як ви правильно зазначаєте) - це деталь реалізації, що представляє загальний субдомен, який реалізує власний згуртований механізм. Ваша модель ні в якому разі не повинна залежати від того, щоб будь-яка частина інфраструктури CQRS поводилася відповідно до правил вашого бізнесу. Метою DDD є моделювання поведінки системи таким чином, щоб результат був корисним абстрагуванням функціональних вимог вашої основної бізнес-сфери. Якщо вивести будь-яку частину такої поведінки з вашої моделі, як би не було спокусливо, це зменшує цілісність і згуртованість вашої моделі (і робить її менш корисною).

Просто розширивши ваш приклад, щоб включити ChangeEmailкоманду, ми можемо ідеально проілюструвати, чому ви не хочете жодної вашої бізнес-логіки у вашій командній інфраструктурі, як вам потрібно було б дублювати свої правила:

  • Повідомлення не може бути порожнім
  • не може бути більше 100 символів
  • електронна пошта повинна бути унікальною

Отже, тепер, коли ми можемо бути впевнені, що наша логіка повинна бути в нашому домені, давайте займемося питанням "куди". Перші два правила можна легко застосувати до нашого Userсукупності, але це останнє правило трохи більш нюансовано; той, який потребує певного подальшого скорочення знань, щоб отримати глибше розуміння. На поверхні може здатися, що це правило стосується а User, але насправді це не так. "Унікальність" електронного листа застосовується до колекції Users(відповідно до деякого обсягу).

А-а-а! Зважаючи на це, стає ясно, що ваша UserRepository(ваша колекція пам’яті Users) може бути кращим кандидатом для застосування цього інваріанта. Метод "збереження", ймовірно, є найбільш розумним місцем для включення чека (куди можна кинути UserEmailAlreadyExistsвиняток). Крім того, домен UserServiceможе бути відповідальним за створення нових Usersта оновлення своїх атрибутів.

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


2
Я згоден з цим. Моє читання до цих пір (без CQRS) говорить мені, що для захисту інваріантів завжди слід перевіряти перевірку в доменній моделі. Тепер я читаю CQRS, це говорить мені, щоб поставити перевірку в об'єкти Command. Це здається протилежним інтуїтивно зрозумілим. Чи знаєте ви будь-які приклади, наприклад, на GitHub, де перевірка проставляється в доменній моделі замість команди? +1.
w0051977
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.