Концептуальное несоответствие между DDD Application Services и REST API

20

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

В DDD клиентам доменной модели необходимо пройти процедурный уровень «Службы приложений», чтобы получить доступ к любой бизнес-функциональности, реализованной сущностями и доменными службами. Например, есть сервис приложений с двумя методами для обновления сущности User:

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

API этой службы приложений предоставляет команды (глаголы, процедуры), а не состояния.

Но если нам также нужно предоставить RESTful API для того же приложения, то существует модель ресурсов пользователя, которая выглядит следующим образом:

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

Ресурсно-ориентированный API предоставляет состояние , а не команды . Это вызывает следующие проблемы:

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

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

  • Вызов методов службы приложений в другом порядке может иметь другой эффект, в то время как REST API делает его похожим, что нет никакой разницы (в пределах одного ресурса)

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

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

Вопросов:

  1. Должна ли вся эта сложность обрабатываться (толстым) слоем отображения REST-to-AppService?
  2. Или я что-то упустил в моем понимании DDD / REST?
  3. Может ли REST просто быть непрактичным для демонстрации функциональности моделей предметной области при определенной (довольно низкой) степени сложности?
astreltsov
источник
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 не является проблемой создание более крупной транзакции из нескольких методов, которые изначально предназначались для собственной транзакции (с использованием транзакции REQUIRED). распространения). Часто для этого специального ресурса все еще требуется дополнительная проверка, но он все еще вполне управляем, поскольку атрибуты (предположительно) являются связными.

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

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

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

QBD
источник
Я тоже так думал - создавайте более детальные представления ресурсов, потому что они более удобны для операций записи. Как вы справляетесь с запросами ресурсов, когда они становятся настолько гранулярными? Создавать только для чтения ненормализованные представления, а?
астрельцов
1
Нет, я не имею дестормализованных представлений только для чтения. Я использую стандарт jsonapi.org, и у него есть механизм для включения связанных ресурсов в ответ на данный ресурс. В основном я говорю «дай мне пользователя с ID 1, а также включи его подресурсы по электронной почте и активации». Это помогает избавиться от дополнительных вызовов REST для подресурсов и не влияет на сложность работы клиента с подресурсами, если вы используете хорошую клиентскую библиотеку JSON API.
QBD
Таким образом, один запрос GET на сервере преобразуется в один или несколько реальных запросов (в зависимости от того, сколько вложенных ресурсов включено), которые затем объединяются в один объект ресурса?
астрельцов
Что если необходимо более одного уровня вложенности?
астрельцов
Да, в реляционных БД это, вероятно, приведет к нескольким запросам. Произвольное вложение поддерживается JSON API, оно описано здесь: 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");
        }
      }
    }

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

codedabbler
источник
0

У меня есть некоторые проблемы, связанные с поиском модели предметной области, ориентированной на ресурсы.

Вы не должны раскрывать модель предметной области. Вы должны показывать приложение ориентированным на ресурсы способом.

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

Совсем нет - отправляйте команды ресурсам приложения, которые взаимодействуют с моделью домена.

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

Да, хотя есть немного другой способ записать это, что может сделать вещи проще; каждая операция обновления API REST отображается на процесс, который отправляет команды одному или нескольким агрегатам.

каждая операция обновления выглядит как атомарная для клиента REST API, но она не реализована таким образом. Каждый вызов службы приложений разработан как отдельная транзакция. Обновление одного поля в модели ресурсов может изменить правила проверки для других полей. Поэтому нам нужно проверить все поля модели ресурсов вместе, чтобы убедиться, что все потенциальные вызовы службы приложений действительны, прежде чем мы начнем их делать. Одновременная проверка набора команд гораздо менее тривиальна, чем выполнение по одной. Как мы можем это сделать на клиенте, который даже не знает, что существуют отдельные команды?

Вы преследуете не тот хвост здесь.

Представьте себе: полностью уберите REST из картинки. Вместо этого представьте, что вы пишете интерфейс рабочего стола для этого приложения. Далее давайте представим, что у вас действительно хорошие требования к дизайну, и вы реализуете пользовательский интерфейс на основе задач. Таким образом, пользователь получает минималистский интерфейс, который идеально настроен для задачи, над которой он работает; пользователь указывает некоторые входные данные, а затем нажимает "VERB!" кнопка.

Что происходит сейчас? С точки зрения пользователя, это единственная атомарная задача, которую необходимо выполнить. С точки зрения domainModel, это количество команд, выполняемых агрегатами, где каждая команда выполняется в отдельной транзакции. Это абсолютно несовместимо! Нам нужно что-то посередине, чтобы преодолеть разрыв!

Что-то это «приложение».

На успешном пути приложение получает некоторое DTO и анализирует этот объект, чтобы получить сообщение, которое оно понимает, и использует данные в сообщении для создания правильно сформированных команд для одного или нескольких агрегатов. Приложение удостоверится, что каждая из команд, которые оно отправляет агрегатам, правильно сформировано (это антикоррупционный слой в работе), и оно загрузит агрегаты и сохранит агрегаты, если транзакция завершится успешно. Агрегат сам решит, допустима ли команда, учитывая ее текущее состояние.

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

Теперь представьте, что у вас есть это приложение; как вы взаимодействуете с ним RESTful способом?

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

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

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

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

В модели чтения есть аналогичное концептуальное несоответствие. Опять же, рассмотрим интерфейс на основе задач; если задача требует изменения нескольких агрегатов, то пользовательский интерфейс для подготовки задачи, вероятно, содержит данные из нескольких агрегатов. Если ваша схема ресурсов 1: 1 с агрегатами, это будет сложно организовать; вместо этого предоставьте ресурс, который возвращает представление данных из нескольких агрегатов, вместе с гипермедиа-элементом управления, который отображает отношение «запуск задачи» к конечной точке задачи, как обсуждалось выше.

Смотрите также: ОТДЫХ на практике от Джима Уэббера.

VoiceOfUnreason
источник
Если мы разрабатываем API для взаимодействия с нашим доменом в соответствии с нашими вариантами использования. Почему бы не спроектировать вещи таким образом, чтобы Sagas вообще не требовался? Может быть, я что-то упускаю, но, читая ваш ответ, я искренне верю, что REST не очень подходит для DDD и лучше использовать удаленные процедуры (RPC). DDD ориентирован на поведение, а REST - на http-глагол. Почему бы не удалить REST с картинки и выставить поведение (команды) в API? В конце концов, вероятно, они были разработаны для удовлетворения сценариев использования, а пробные транзакции. В чем преимущество REST, если у нас есть пользовательский интерфейс?
иберодев