Концептуальна невідповідність між прикладними службами DDD та API REST


20

Я намагаюся створити додаток, який має складний бізнес-домен та вимогу підтримувати REST API (не суворо REST, а орієнтований на ресурси). У мене виникають проблеми з розкриттям доменної моделі орієнтованим на ресурси.

У DDD клієнтам доменної моделі необхідно пройти процедурний рівень «Служби прикладних програм», щоб отримати доступ до будь-якої ділової функціональності, реалізованої Entities and Domain Services. Наприклад, є служба додатків із двома способами оновлення сутності користувача:

userService.ChangeName(name);
userService.ChangeEmail(email);

API цієї Служби програм розкриває команди (дієслова, процедури), а не стан.

Але якщо нам також потрібно надати API RESTful для того самого додатка, то існує модель User Resource, яка виглядає приблизно так:

{
name:"name",
email:"email@mail.com"
}

API, орієнтований на ресурси, відкриває стан , а не команди . Це викликає такі проблеми:

  • кожна операція оновлення для API REST може відображати один або декілька викликів процедури служби служби додатків, залежно від того, які властивості оновлюються в моделі ресурсу

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

  • виклик методів служби служб у різному порядку може мати різний ефект, тоді як API REST дозволяє виглядати, що немає різниці (в межах одного ресурсу)

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

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

Запитання:

  1. Чи повинна вся ця складність оброблятися (товстим) шаром відображення REST-to-AppService?
  2. Або я щось пропускаю в розумінні DDD / REST?
  3. Чи може REST просто не бути практичним для виявлення функціональності доменних моделей над певним (досить низьким) ступенем складності?

3
Я особисто не вважаю REST необхідним. Однак можна ввімкнути DDD в нього: infoq.com/articles/rest-api-on-cqrs programmers.stackexchange.com/questions/242884/… blog.42.nl/articles/rest-and-ddd-incompatible
День

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

2
@astr Проста відповідь полягає в тому, що ресурси не є вашою моделлю, тому дизайн коду обробки ресурсів не повинен впливати на дизайн вашої моделі. Ресурси - це зовнішній аспект системи, коли модель є внутрішньою. Думайте про ресурси так само, як і про інтерфейс користувача. Користувач може натиснути одну кнопку на інтерфейсі користувача і в моделі трапляється сто різних речей. Схожий на ресурс. Клієнт оновлює ресурс (одна заява PUT) і в моделі може трапитися мільйон різних речей. Це анти-модель, щоб зв'язати вашу модель тісно з вашими ресурсами.
Кормак Малхолл

1
Це хороша розмова про трактування дій у вашому домені як побічних ефектів змін стану REST, зберігаючи окремо ваш домен та Інтернет (швидкий вперед до 25 хв. За соковитий шматочок) yow.eventer.com/events/1004/talks/1047
Кормак Малхолл

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

Відповіді:


10

У мене була та сама проблема, і я вирішив її, по-різному моделюючи REST-ресурси, наприклад:

/users/1  (contains basic user attributes) 
/users/1/email 
/users/1/activation 
/users/1/address

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

Кожна операція на цих ресурсах є атомною, хоча вона може бути реалізована за допомогою декількох методів обслуговування - принаймні у Spring / Java EE це не проблема для створення більшої транзакції з декількох методів, які спочатку мали намір мати власну транзакцію (використовуючи ЗАПОВІДНУ транзакцію поширення). Вам часто все-таки потрібно зробити додаткову перевірку цього спеціального ресурсу, але він все ще досить керований, оскільки атрибути є (мабуть, є згуртованими).

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

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

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


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

1
Ні, у мене немає денормалізованих уявлень лише для читання. Я використовую стандарт jsonapi.org, і він має механізм включення відповідних ресурсів у відповідь на даний ресурс. В основному я кажу "дайте мені Користувача з ідентифікатором 1, а також включіть його підресурси електронною поштою та активацію". Це допомагає позбутися від зайвих REST-викликів для субресурсів, і це не впливає на складність роботи клієнта з субресурсами, якщо ви використовуєте хорошу клієнтську бібліотеку API JSON.
qbd

Отже, один запит GET на сервері перетворюється на один або більше фактичних запитів (залежно від того, скільки підресурсів включено), які потім об'єднуються в один об’єкт ресурсу?
астрельцов

Що робити, якщо необхідно більше одного рівня гніздування?
астрельцов

Так, у реляційних dbs це, ймовірно, перекладається на кілька запитів. Довільне вкладення підтримується API JSON, воно описане тут: jsonapi.org/format/#fetching-includes
qbd

0

Основне питання тут полягає в тому, як прозора виклик бізнес-логіки під час виклику REST? Це проблема, яку REST безпосередньо не вирішує.

Я вирішив це, створивши власний рівень управління даними над постійним провайдером, таким як JPA. Використовуючи мета-модель із спеціальними анотаціями, ми можемо викликати відповідну бізнес-логіку, коли стан сутності змінюється. Це гарантує, що незалежно від того, як змінюється держава суб'єкта господарювання, застосовується логіка бізнесу. Він зберігає вашу архітектуру ДУХО, а також вашу логіку бізнесу в одному місці.

Використовуючи наведений вище приклад, ми можемо викликати метод логіки бізнесу під назвою validateName, коли поле імені змінюється за допомогою REST:

class User { 
      String name;
      String email;

      /**
       * This method will be transparently invoked when the value of name is changed
       * by REST.
       * The XorUpdate annotation becomes effective for PUT/POST actions
       */
      @XorPostChange
      public void validateName() {
        if(name == null) {
          throw new IllegalStateException("Name cannot be set as null");
        }
      }
    }

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


0

У мене виникають проблеми з розкриттям доменної моделі орієнтованим на ресурси.

Ви не повинні піддавати доменну модель орієнтованою на ресурси. Вам слід виставляти додаток орієнтованим на ресурси.

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

Зовсім не - надсилайте команди на ресурси додатків, які взаємодіють із моделлю домену.

кожна операція оновлення для API REST може відображати один або декілька викликів процедури служби служби додатків, залежно від того, які властивості оновлюються в моделі ресурсу

Так, хоча є дещо інший спосіб написання цього, що може зробити речі простішими; кожна операція оновлення api REST відображає процес, який передає команди до одного або декількох агрегатів.

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

Ви переслідуєте тут неправильний хвіст.

Уявіть: вийміть REST із зображення повністю. Уявіть собі, що ви писали настільний інтерфейс для цієї програми. Давайте уявимо, що у вас справді хороші вимоги до дизайну та реалізуєте інтерфейс, що базується на завданнях. Таким чином, користувач отримує мінімалістичний інтерфейс, ідеально налаштований на завдання, над яким він працює; користувач вказує деякі входи, після чого натискає "VERB!" кнопка.

Що відбувається зараз? З точки зору користувача, це єдина атомна задача, яку потрібно виконати. З точки зору domainModel, це ряд команд, які виконуються агрегатами, де кожна команда виконується в окремій транзакції. Це абсолютно несумісні! Нам потрібно щось посередині, щоб подолати розрив!

Щось - це "додаток".

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

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

А тепер уявіть, що у вас створено цю програму; як ти взаємодієш з нею в RESTful способі?

  1. Клієнт починає з опису гіпермедіа свого поточного стану (тобто: інтерфейсу, заснованого на завданнях), включаючи управління гіпермедіа.
  2. Клієнт відправляє представлення завдання (тобто: DTO) на ресурс.
  3. Ресурс аналізує вхідний запит HTTP, захоплює представлення та передає його програмі.
  4. Додаток виконує завдання; з погляду ресурсу, це чорна скринька, яка має один із наступних результатів
    • програма успішно оновила всі агрегати: ресурс повідомляє про успіх клієнту, направляючи його в новий стан програми
    • антикорупційний шар відхиляє повідомлення: ресурс повідомляє клієнту 4хх помилку (можливо, поганий запит), можливо передаючи опис виниклої проблеми.
    • додаток оновлює деякі агрегати: ресурс повідомляє клієнту про те, що команда була прийнята, і спрямовує клієнта на ресурс, який забезпечить подання про виконання команди.

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

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

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

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

Дивіться також: REST на практиці від Джима Вебера.


Якщо ми розробляємо API для взаємодії з нашим доменом відповідно до наших випадків використання. Чому б не спроектувати речі таким чином, щоб Sagas взагалі не вимагався? Можливо, мені чогось не вистачає, але, читаючи вашу відповідь, я справді вважаю, що REST не дуже добре відповідає DDD, і краще використовувати віддалені процедури (RPC). DDD орієнтований на поведінку, тоді як REST - http-verb centric. Чому б не видалити REST із зображення та не розкрити поведінку (команди) в API? Зрештою, ймовірно, вони були розроблені для задоволення сценаріїв використання випадків, а проблема є транзакційною. Яка перевага REST, якщо ми володіємо інтерфейсом?
iberodev
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.