Мартин Фаулер в "Шаблоны архитектуры корпоративных приложений” написал:
I found this(business logic) a curious term because there are few things that are less logical than business logic.
Первоначальный перевод можно понять так: бизнес-логика — очень нелогичная логика.
Ведь во многих случаях бизнес-логику программного обеспечения невозможно получить путем рассуждений, а иногда даже представить себе. В результате и без того сложный бизнес стал более сложным и трудным для понимания. При внедрении конкретного кодирования, помимо работы со сложностью бизнеса, нельзя игнорировать сложность технологии.Например, мы должны обращать внимание на техническое разделение, следовать основным принципам разработки программного обеспечения и учитывать производительность и безопасность и т.д.
Во многих проектах техническая сложность и бизнес-сложность переплетаются. Однако при разумном проектировании технологии и бизнес могут быть разделены или, по крайней мере, связь между ними может быть уменьшена. Среди различных методов моделирования программного обеспечения,Дизайн, управляемый доменом(Domain Driven Design, DDD) пытается решить проблему сложности программного обеспечения с помощью своих собственных принципов и процедур. Он фокусирует внимание разработчиков в первую очередь на самом бизнесе и делает техническую архитектуру и реализацию кода неотъемлемой частью процесса моделирования программного обеспечения. , "побочный продукт".
Обзор DDD
DDD делится на стратегический дизайн и тактический дизайн. При стратегическом проектировании мы фокусируемся на поддоменах иОграниченный контекст (BC)Разделение , а также восходящие и нисходящие отношения между каждым ограничивающим контекстом. Нынешнее предложение «использовать DDD в микросервисах» очень горячо, и его первоначальная логика — не что иное, как «ограниченный контекст в DDD может использоваться для управления разделением сервисов в микросервисах». На самом деле Bounded Context по-прежнему является проявлением модульности программного обеспечения, и движущая сила модульного принципа, которого мы придерживались, та же самая, то есть сделать программную систему более организованной в человеческом мозгу с помощью определенных средств, сделать ее проще. для людей, которые "цель" понять и контролировать программную систему.
Если стратегический дизайн более склонен к архитектуре программного обеспечения, то тактический дизайн более склонен к реализации кода. Цель тактического дизайна DDD состоит в том, чтобы позволить бизнесу быть отделенным и выделенным от технологии, чтобы код мог непосредственно выражать сам бизнес, включая такие понятия, как корень агрегации, служба приложений, библиотека ресурсов и фабрика. Хотя DDD не обязательно реализуется объектно-ориентированным (ОО), мы обычно используем парадигму объектно-ориентированного программирования при работе с DDD. принципы (например,SOLID) по-прежнему хранится в DDD. В этой статье в основном объясняется тактический дизайн DDD.
В этой статье в качестве примера взята простая система заказов электронной коммерции.Исходный код можно получить следующими способами:
git clone https://github.com/e-commerce-sample/order-backend
git checkout a443dace
3 распространенных способа сделать бизнес
Прежде чем объяснять DDD, давайте рассмотрим несколько распространенных способов реализации бизнес-кода.В примере проекта есть бизнес-требование «изменить количество продуктов в заказе» следующим образом:
Количество Товара в Заказе может быть изменено, но только в том случае, если Заказ находится в неоплаченном состоянии, а общая Цена Заказа должна быть обновлена после изменения количества Товара.
1. Внедрение на основе модели «Сервис + Анемия».
Этот метод в настоящее время используется многими программными проектами.Основные особенности: есть анемичный «объект предметной области», бизнес-логика реализуется через класс службы, затем объект предметной области обновляется с помощью метода установки, и, наконец, через методDAO(В большинстве случаев может использоваться структура ORM, такая как Hibernate) в базу данных. Реализуйте класс OrderService следующим образом:
@Transactional
public void changeProductCount(String id, ChangeProductCountCommand command) {
Order order = DAO.findById(id);
if (order.getStatus() == PAID) {
throw new OrderCannotBeModifiedException(id);
}
OrderItem orderItem = order.getOrderItem(command.getProductId());
orderItem.setCount(command.getCount());
order.setTotalPrice(calculateTotalPrice(order));
DAO.saveOrUpdate(order);
}
Этот подход по-прежнему является процедурной парадигмой программирования, которая нарушает самые основные принципы объектно-ориентированного программирования. Еще одной проблемой является нечеткое разделение обязанностей, которое должноOrder
Бизнес-логика просочилась в другом месте (OrderService
), привести кOrder
стать контейнером данныхАнемичная модель, а не реальная модель предметной области. В ходе непрерывной эволюции проекта эта бизнес-логика будет разбросана по разным классам Сервиса, и конечным результатом будет то, что код становится все более сложным для понимания и постепенно теряет способность к расширению.
2. Реализация на основе скрипта транзакции
В предыдущей реализации мы бы нашли объекты предметной области (Order
) существует только для того, чтобы такие инструменты, как ORM, сохранялись один раз, а без ORM объекты домена даже не должны существовать. Поэтому реализация кода в это время вырождается вСкрипт транзакции, то есть результат, рассчитанный в классе Service, напрямую сохраняется в БД (а иногда класса Service нет, а бизнес-логика напрямую реализуется через SQL):
@Transactional
public void changeProductCount(String id, ChangeProductCountCommand command) {
OrderStatus orderStatus = DAO.getOrderStatus(id);
if (orderStatus == PAID) {
throw new OrderCannotBeModifiedException(id);
}
DAO.updateProductCount(id, command.getProductId(), command.getCount());
DAO.updateTotalPrice(id);
}
Видно, что в DAO гораздо больше методов, на данный момент DAO уже не просто инкапсуляция персистентности, но и содержит бизнес-логику. Кроме того,DAO.updateTotalPrice(id)
При реализации метода SQL будет вызываться напрямую для обновления общей стоимости Заказа. Подобно подходу «сервис + анемия», сценарии транзакций также имеют проблему разбросанной бизнес-логики.
На самом деле скрипты транзакций не являются полным антишаблоном и могут использоваться, если система достаточно проста. Однако: с одной стороны, степень «простоты» понять нелегко, с другой стороны, программные системы обычно добавляют больше функций в процессе непрерывной эволюции, делая изначально простой код постепенно усложняющимся. Поэтому сценарии транзакций мало используются в практических приложениях.
3. Реализация на основе объектов предметной области
Таким образом, основная бизнес-логика согласуется с объектами предметной области с хорошим поведением (Order
), пониматьOrder
Классы следующие:
public void changeProductCount(ProductId productId, int count) {
if (this.status == PAID) {
throw new OrderCannotBeModifiedException(this.id);
}
OrderItem orderItem = retrieveItem(productId);
orderItem.updateCount(count);
}
Затем в Controller или Service вызовитеOrder.changeProductCount()
:
@PostMapping("/order/{id}/products")
public void changeProductCount(@PathVariable(name = "id") String id, @RequestBody @Valid ChangeProductCountCommand command) {
Order order = DAO.byId(orderId(id));
order.changeProductCount(ProductId.productId(command.getProductId()), command.getCount());
order.updateTotalPrice();
DAO.saveOrUpdate(order);
}
Видно, что все услуги («Проверить статус заказа», «Изменить количество товара» и «Обновить общую стоимость заказа») включены вOrder
объекты, этоOrder
должны иметь обязанности. (Однако в примере кода есть место, явно нарушающее принцип связности, о чем будет сказано ниже. Как читатель саспенса, вы можете сначала попытаться найти его)
По сути, этот метод очень похож на тактический режим DDD, обсуждаемый в этой статье, но DDD абстрагирует больше концепций и принципов.
субподряд на основе бизнеса
в этой серииПредыдущая:Шаблон проекта Spring BootВ статье, по сути, я уже говорил о субподряде на основе бизнеса в сочетании со сценарием DDD, вот краткое обсуждение. Так называемый субподряд на основе бизнеса относится к модульному разделению бизнес-функций, реализуемому программным обеспечением, а не к разделению с технической точки зрения (например, сначалаservice
иinfrastruture
упаковка и т.д.). При стратегическом проектировании DDD мы фокусируемся на рассмотрении всей программной системы с точки зрения макроэкономики, а затем разделяем систему на поддомены и ограниченные контексты по определенным принципам. В тактической практике мы также планируем общую структуру кодекса с помощью аналогичного метода набросков, и принятые принципы все еще не могут избежать основных принципов «сплоченности» и «разделения обязанностей». На этом этапе первое, что бросается в глаза, — это субподряд программного обеспечения.
В DDD корень агрегации (описанный ниже) является носителем основной бизнес-логики и типичным представителем принципа «сплоченности», поэтому обычной практикой является разделение пакета верхнего уровня на основе корня агрегации. В примере проекта электронной коммерции есть два совокупных корневых объекта.Order
иProduct
, созданный отдельноorder
пакет иproduct
пакет, а затем разделите подпакеты в соответствии со сложностью структуры кода в соответствующих пакетах верхнего уровня, например, дляproduct
Сумка:
└── product
├── CreateProductCommand.java
├── Product.java
├── ProductApplicationService.java
├── ProductController.java
├── ProductId.java
├── ProductNotFoundException.java
├── ProductRepository.java
└── representation
├── ProductRepresentationService.java
└── ProductSummaryRepresentation.java
можно увидеть,ProductRepository
иProductController
Подождите, пока большинство классов будут размещены напрямуюproduct
пакет, без отдельного субподряда, но класс товарного видаProductSummaryRepresentation
Но сделайте отдельный субподряд. Принцип здесь таков: после того, как все классы объединены вproduct
В случае с пакетами, если структура кода достаточно проста, нет необходимости снова разделять подпакеты.ProductRepository
иProductController
Это так; и если несколько классов должны снова быть связаны, они должны быть заключены в субподряд, например, через REST.
Когда интерфейс API возвращает данные о продукте, в коде участвуют два объекта.ProductRepresentationService
иProductSummaryRepresentation
, эти два объекта тесно связаны между собой, поэтому поместите их вrepresentation
подпакет. Для более сложных Заказов субподряд заключается в следующем:
├── order
│ ├── OrderApplicationService.java
│ ├── OrderController.java
│ ├── OrderPaymentProxy.java
│ ├── OrderPaymentService.java
│ ├── OrderRepository.java
│ ├── command
│ │ ├── ChangeAddressDetailCommand.java
│ │ ├── CreateOrderCommand.java
│ │ ├── OrderItemCommand.java
│ │ ├── PayOrderCommand.java
│ │ └── UpdateProductCountCommand.java
│ ├── exception
│ │ ├── OrderCannotBeModifiedException.java
│ │ ├── OrderNotFoundException.java
│ │ ├── PaidPriceNotSameWithOrderPriceException.java
│ │ └── ProductNotInOrderException.java
│ ├── model
│ │ ├── Order.java
│ │ ├── OrderFactory.java
│ │ ├── OrderId.java
│ │ ├── OrderIdGenerator.java
│ │ ├── OrderItem.java
│ │ └── OrderStatus.java
│ └── representation
│ ├── OrderItemRepresentation.java
│ ├── OrderRepresentation.java
│ └── OrderRepresentationService.java
Как видите, мы специально создалиmodel
В пакете размещаются все объекты предметной области, относящиеся к корню агрегата Order, кроме того, по принципу однотипного агрегата создаютсяcommand
пакет иexception
Пакеты используются для размещения классов запросов и классов исключений соответственно.
Фасад модели предметной области — службы приложений
Концепция варианта использования (Use Case) в UML представляет собой базовую логическую единицу, которую программное обеспечение обеспечивает внешними бизнес-функциями. В DDD, поскольку бизнес упоминается как первый приоритет, мы, естественно, надеемся, что обработка бизнеса может быть проявлена.Для достижения этой цели DDD предоставляет уровень абстракции, называемый ApplicationService. ApplicationService используетузор фасада, как общий вход и выход модели предметной области для предоставления бизнес-функций внешнему миру, точно так же, как стойка регистрации отеля обрабатывает различные потребности клиентов.
При написании кода для реализации бизнес-функций обычно используются два рабочих процесса:
- Восходящее: сначала разработайте модель данных, например структуру таблицы реляционной базы данных, а затем реализуйте бизнес-логику. Когда я занимаюсь программированием в паре с разными программистами, я всегда слышу это предложение: «Позвольте мне сначала спроектировать поля таблицы базы данных». Этот подход отдает приоритет технической модели данных, а не модели предметной области, представляющей бизнес, в отличие от DDD.
- Сверху вниз: получите бизнес-требование, сначала определите формат данных запроса с клиентом, затем внедрите Controller и ApplicationService, затем внедрите модель предметной области (модель предметной области в настоящее время обычно идентифицируется) и, наконец, реализуйте постоянство.
В практике DDD, естественно, должна применяться нисходящая реализация. Реализация ApplicationService следует очень простому принципу, то есть бизнес-вариант использования соответствует бизнес-методу в ApplicationService. Например, для упомянутого выше бизнес-требования «изменить количество Товаров в Заказе» реализуется следующим образом:
Реализовать OrderApplicationService:
@Transactional
public void changeProductCount(String id, ChangeProductCountCommand command) {
Order order = orderRepository.byId(orderId(id));
order.changeProductCount(ProductId.productId(command.getProductId()), command.getCount());
orderRepository.save(order);
}
OrderController вызывает OrderApplicationService:
@PostMapping("/{id}/products")
public void changeProductCount(@PathVariable(name = "id") String id, @RequestBody @Valid ChangeProductCountCommand command) {
orderApplicationService.changeProductCount(id, command);
}
В настоящее время,order.changeProductCount()
иorderRepository.save()
Не обязательно реализовывать, ноOrderController
иOrderApplicationService
Создана основа для бизнес-процессинга.
Как видите, вариант использования «изменить количество продуктов в заказе»OrderApplicationService.changeProductCount()
В реализации метода всего несколько 3 строк кода, однако такому простому ApplicationService уделено много внимания.
ApplicationService должен следовать следующим принципам:
- Существует однозначное соответствие между бизнес-методами и бизнес-прецедентами: я уже упоминал их ранее, поэтому не буду повторяться.
- Между бизнес-методами и транзакциями существует однозначное соответствие: то есть каждый бизнес-метод представляет собой независимую границу транзакции.
OrderApplicationService.changeProductCount()
Методы, отмеченные Spring@Transactional
Аннотация, указывающая, что весь метод инкапсулирован в транзакцию. - Он не должен содержать саму бизнес-логику: бизнес-логика должна быть реализована в модели предметной области, точнее в корне агрегата, в данном случае,
order.changeProductCount()
В методе действительно реализована бизнес-логика, а ApplicationService просто вызывается как прокси.order.changeProductCount()
метод, поэтому ApplicationService должен быть тонким слоем. - Он не имеет ничего общего с UI или протоколом связи: ApplicationService позиционируется не как фасад всей программной системы, а как фасад модели предметной области, а это значит, что ApplicationService не должен заниматься техническими деталями, такими как взаимодействие с UI или протокол связи. В этом примере Контроллер как вызывающая сторона ApplicationService отвечает за обработку протокола связи (HTTP) и прямое взаимодействие с клиентом. Этот метод обработки делает ApplicationService универсальным, то есть независимо от того, является ли конечный вызывающий объект HTTP-клиентом, RPC-клиентом или даже функцией Main, доступ к модели предметной области возможен только через ApplicationService.
- Принимать примитивные типы данных: ApplicationService действует как вызывающая сторона модели предметной области, и детали реализации модели предметной области должны быть для него черным ящиком, поэтому ApplicationService не должен ссылаться на объекты в модели предметной области. Кроме того, данные в объекте запроса, принимаемом ApplicationService, используются только для описания самого бизнес-запроса, и они должны быть максимально простыми при условии, что бизнес-требования могут быть выполнены. Поэтому ApplicationService обычно имеет дело с некоторыми примитивными типами данных. В этом примере
OrderApplicationService
Принятый заказ ID — это исходный тип String Java, который при вызове инкапсулируется как репозиторий в модели предметной области.OrderId
объект.
Субъект бизнеса - Совокупный корень
Проще говоря, Aggregate Root (AR) — это наиболее важные объекты предметной области в модели программного обеспечения, которые существуют в форме существительных, таких как пример проекта в этой статье.Order
иProduct
. Другой пример: для системы управления участниками Member является совокупным корнем, для системы возмещения Expense является совокупным корнем, для страховой системы Policy является совокупным корнем. Агрегатный корень является основным носителем бизнес-логики, и все тактические реализации в DDD вращаются вокруг агрегатного корня.
Однако не все существительные в модели предметной области могут быть смоделированы как совокупные корни. Так называемая «агрегация», как следует из названия, должна собрать воедино очень связанные концепции в этой области, чтобы сформировать единое целое. Что касается того, какие концепции могут сойтись, нам нужно глубокое понимание самого бизнеса, поэтому DDD подчеркивает необходимость работы команд разработчиков с экспертами в предметной области. популярный в последние годыбуря событийМоделирование деятельности, его первоначальное намерение состоит в том, чтобы перечислить все события в домене, чтобы мы могли полностью понять бизнес в домене, а затем определить совокупный корень.
Для варианта использования «Обновить количество продуктов в заказе» совокупный кореньOrder
Реализация выглядит следующим образом:
public void changeProductCount(ProductId productId, int count) {
if (this.status == PAID) {
throw new OrderCannotBeModifiedException(this.id);
}
OrderItem orderItem = retrieveItem(productId);
orderItem.updateCount(count);
this.totalPrice = calculateTotalPrice();
}
private BigDecimal calculateTotalPrice() {
return items.stream()
.map(OrderItem::totalPrice)
.reduce(ZERO, BigDecimal::add);
}
private OrderItem retrieveItem(ProductId productId) {
return items.stream()
.filter(item -> item.getProductId().equals(productId))
.findFirst()
.orElseThrow(() -> new ProductNotInOrderException(productId, id));
}
В этом примереOrder
Предметы в (orderItems
) и общая цена (totalPrice
) тесно связаны,orderItems
изменения приведут непосредственно кtotalPrice
изменения, поэтому оба должны, естественно, быть сплоченными вOrder
Вниз. также,totalPrice
ИзменениеorderItems
Неизбежный результат изменений, эта причинно-следственная связь управляется бизнесом, чтобы обеспечить эту «неизбежность», нам нужноOrder.changeProductCount()
И «причина», и «следствие» реализованы в методе, то есть совокупный корень должен обеспечивать бизнес-непротиворечивость. В DDD деловая согласованность называетсяИнварианты.
Помните упомянутое выше «приостановление нарушения сплоченности»? позвони в это времяOrder
Методы ведения бизнеса следующие:
.....
order.changeProductCount(ProductId.productId(command.getProductId()), command.getCount());
order.updateTotalPrice();
.....
Для реализации бизнес-функции «обновить количество товаров в заказе» здесь последовательно вызываютсяOrder
Два общедоступных метода наchangeProductCount()
иupdateTotalPrice()
. Хотя этот подход также правильно реализует бизнес-логику, он оставляет ответственность за обеспечение согласованности бизнеса наOrder
вызывающий (контроллер выше) вместоOrder
вызывающая сторона должна убедиться, чтоchangeProductCount()
должен вызываться послеupdateTotalPrice()
метод, этот аспектOrder
С другой стороны, вызывающий абонент не берет на себя такие обязанности, иOrder
Это ответственность, которую следует взять на себя.
Стремление к сплоченности естественным образом выходит за пределы совокупных корней. В стратегическом дизайне DDD мы разделили большую программную систему на разные «модули» посредством разделения ограниченных контекстов.Согласно этой предпосылке, обсуждение связности в ограниченном контексте будет лучше, чем обсуждение связности в ограниченном контексте.большой шар грязиОбсуждения в системе становятся намного проще.
Дизайн совокупных корней, которых следует остерегатьсяОбъект Бога, то есть для реализации всех бизнес-функций используется большой и всеобъемлющий предметный объект. За объектом Бога скрывается, казалось бы, правдоподобная логика: поскольку нам нужна связность, давайте объединим все связанные вещи, например, с помощьюProduct
Класс для обработки всех бизнес-сценариев, включая заказы, логистику, счета и т. д. Этот механический подход кажется сплоченным, но на самом деле он противоположен сплоченности. Для решения таких задач по-прежнему необходимо прибегать к ограниченным контекстам, а разные ограниченные контексты используют свои собственныеВездесущий языкУниверсальный язык требует, чтобы бизнес-концепция не была двусмысленной, в соответствии с этим принципом разные ограниченные контексты могут иметь свои собственныеProduct
Классы, несмотря на одно и то же название, воплощают разные виды деятельности.
Помимо сплоченности и согласованности, агрегатные корни обладают следующими характеристиками:
- Реализация агрегированного корня не должна иметь ничего общего с фреймворком: поскольку DDD подчеркивает разделение сложности бизнеса и технической сложности, агрегированный корень как основной носитель бизнеса должен относиться к возможностям на уровне технической фреймворка как минимум возможно, желательноPOJO. Только представьте, если ваш проект когда-нибудь понадобится перенести с Spring наPlay, и вы можете с уверенностью сказать своему боссу, что вы можете напрямую скопировать основной код Java, что это будет замечательный опыт. Другими словами, много раз техническая структура будет иметь «большой шаг» обновления, что приведет к изменениям в API в структуре и больше не будет поддерживать обратную совместимость.В настоящее время, если наша модель предметной области не имеет ничего общего с framework, то мы можем сделать Чтобы пережить процесс обновления фреймворка.
- Ссылка между корнями агрегации выполняется по идентификатору: если граница корня агрегации спроектирована правильно, только один корень агрегации будет обновляться одновременно для бизнес-варианта. весь другой совокупный корень в этом совокупном корне? В этом примере
Order
внизOrderItem
ЦитируетсяProductId
, вместо всегоProduct
. - Все изменения в корне агрегации должны выполняться через корень агрегации: чтобы обеспечить согласованность корня агрегации и предотвратить утечку внутренней логики корня агрегации наружу, клиент может использовать весь корень агрегации только как унифицированный ввод вызовов.
- Если транзакции необходимо обновить несколько корней агрегирования, сначала подумайте, нет ли проблемы с обработкой границ вашего собственного корня агрегирования, потому что обычно не существует сценария, при котором транзакция обновляет несколько корней агрегирования при разумном проектировании. Если такая ситуация действительно требуется бизнесу, то рассмотрите возможность введениямеханизм сообщенийисобытийно-ориентированная архитектура, чтобы убедиться, что транзакция обновляет только один корень агрегата, а затем асинхронно обновляет другие корни агрегатов с помощью механизма сообщений.
-
Совокупные корни не должны ссылаться на инфраструктуру.
- Внешний мир не должен содержать структуры данных внутри корня агрегата.
- Старайтесь использовать мелкие агрегаты.
Сущности против объектов-значений
В программной модели есть объекты-сущности (Entity) и объекты-значения (Value Object).На самом деле, этот метод деления не является исключительным для DDD, но мы подчеркиваем разницу между ними в DDD.
Сущностный объект представляет собой объект с определенным жизненным циклом и глобальным уникальным идентификатором (ID), например, в этой статье.Order
иProduct
, в то время как объект значения представляет описательный, неуникально идентифицированный объект, такой какAddress
объект.
Агрегированные корни должны быть объектами сущностей, но не все объекты сущностей являются совокупными корнями, а совокупные корни также могут иметь другие объекты подсущностей. Идентификатор корня агрегата глобально уникален во всей программной системе, а идентификаторы подсущностных объектов под ним должны быть уникальными только для одного корня агрегата. В этом примере проектаOrderItem
совокупный кореньOrder
Объект подсущности под:
public class OrderItem {
private ProductId productId;
private int count;
private BigDecimal itemPrice;
}
Видно, что хотяOrderItem
использовалProductID
как ID, но на данный момент мы не наслаждаемсяProductID
Глобальная уникальность, фактически множественнаяOrder
может содержать одно и то жеProductID
изOrderItem
, то есть несколько заказов могут содержать один и тот же продукт.
Очень важный принцип различения сущностей и объектов-значений заключается в том, чтобы судить на основе равенства.Равенство объектов-сущностей достигается по идентификатору.Для двух сущностей, если все их атрибуты одинаковы, но идентификаторы разные, то они по-прежнему два разные сущности, как пара однояйцевых близнецов, они все же два разных физических лица. Для объектов-значений оценка равенства выполняется через поля атрибутов. Например, адрес доставки под заказAddress
Объект — это типичный объект-значение:
public class Address {
private String province;
private String city;
private String detail;
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Address address = (Address) o;
return province.equals(address.province) &&
city.equals(address.city) &&
detail.equals(address.detail);
}
@Override
public int hashCode() {
return Objects.hash(province, city, detail);
}
}
существуетAddress
изequals()
метод, судяAddress
Все свойства включены (province
,city
,detail
) выбирать между двумяAddress
равенства.
Еще одна особенность объектов стоимости заключается в том, чтоНеизменный, что означает, что после создания объекта-значения его нельзя изменить.Если его необходимо изменить, необходимо заново создать новый объект-значение, чтобы заменить исходный. Например, в примере проекта есть бизнес-требование:
В случае неоплаченных заказов вы можете изменить подробный адрес адреса доставки заказа (подробно)
так какAddress
даOrder
объект в совокупном корне, справаAddress
изменения могут быть внесены толькоOrder
Завершен вOrder
реализовано вchangeAddressDetail()
метод:
public void changeAddressDetail(String detail) {
if (this.status == PAID) {
throw new OrderCannotBeModifiedException(this.id);
}
this.address = this.address.changeDetailTo(detail);
}
Видно, что по вызовуaddress.changeDetailTo()
метод, мы получаем совершенно новыйAddress
объект, то новыйAddress
Весь объект назначаетсяaddress
Атрибуты. В настоящее времяAddress.changeDetailTo()
Реализация выглядит следующим образом:
public Address changeDetailTo(String detail) {
return new Address(this.province, this.city, detail);
}
здесьchangeDetailTo()
метод использует новый подробный адресdetail
и без измененийprovince
,city
воссоздалAddress
объект.
Неизменяемость объектов-значений упрощает логику программы: вам не нужно поддерживать сложную информацию о состоянии, создавать ее, когда она вам нужна, и выбрасывать, когда она вам не нужна, делая объект-значение похожим на пассер -в программе. В моделировании DDD хорошо зарекомендовавшей себя практикой является моделирование бизнес-концепций как объектов ценности в максимально возможной степени.
заOrderItem
Например, как требует наш бизнесOrderItem
Число , то есть оно имеет значение жизненного цикла, поэтому в этой статьеOrderItem
Моделируются как твердые объекты. Однако если такой деловой необходимости нет, тоOrderItem
Моделирование как объект-значение должно быть более подходящим.
Кроме того, следует указать, что деление сущностей и объектов-значений не статично, а должно определяться в соответствии с ограниченным контекстом, в котором оно находится.Одно и то же деловое имя может быть сущностью в одном ограниченном контексте, а находиться в другом ограниченном контексте. является объектом-значением. Например, заказOrder
Он должен быть смоделирован как объект в контексте закупок, но может быть смоделирован как объект стоимости в контексте логистики.
Home of Aggregate Roots — Библиотека ресурсов
С точки зрения непрофессионала, репозиторий используется для сохранения совокупных корней. С технической точки зрения Repository и DAO играют схожие роли, но DAO предназначен только для инкапсуляции тонкого слоя базы данных, в то время как Repository более склонен к модели предметной области. Кроме того, из всех доменных объектов только совокупный корень «заслуживает» владения репозиторием, а DAO не имеют этого ограничения.
выполнитьOrder
библиотека ресурсовOrderRepository
следующее:
public void save(Order order) {
String sql = "INSERT INTO ORDERS (ID, JSON_CONTENT) VALUES (:id, :json) " +
"ON DUPLICATE KEY UPDATE JSON_CONTENT=:json;";
Map<String, String> paramMap = of("id", order.getId().toString(), "json", objectMapper.writeValueAsString(order));
jdbcTemplate.update(sql, paramMap);
}
public Order byId(OrderId id) {
try {
String sql = "SELECT JSON_CONTENT FROM ORDERS WHERE ID=:id;";
return jdbcTemplate.queryForObject(sql, of("id", id.toString()), mapper());
} catch (EmptyResultDataAccessException e) {
throw new OrderNotFoundException(id);
}
}
существуетOrderRepository
, мы только определяемsave()
иbyId()
методы сохранения/обновления совокупных корней и получения совокупных корней по ID соответственно. Эти два метода являются наиболее распространенными в репозитории, и некоторые специалисты по DDD даже считают, что чистый репозиторий должен содержать только эти два метода.
После прочтения у вас могут возникнуть вопросы: почемуOrderRepository
В ? нет таких методов, как обновление и запрос? На самом деле, роль репозитория состоит в том, чтобы предоставить совокупные корни для модели предметной области, подобно «контейнеру» совокупных корней. Самому «контейнеру» все равно, является ли операция клиента над совокупными корнями новой или обновленной. совокупный корневой объект, а Репозиторий отвечает только за синхронизацию его состояния из памяти компьютера с механизмом персистентности, с этой точки зрения Репозиторию нужен только аналогичныйsave()
способ завершения операции синхронизации. Конечно, это результат проектирования с начальной точки концепции.На техническом уровне новые дополнения и обновления по-прежнему должны рассматриваться по-разному.Например, операторы SQL имеютinsert
иupdate
Просто мы скрываем такие технические подробности вsave()
В методе клиентской стороне не нужно знать эти детали. В этом примере мы используем MySQLON DUPLICATE KEY UPDATEЭта функция обрабатывает операции добавления и обновления базы данных. Конечно, мы также можем программно определить, существует ли уже корень агрегата в базе данных, и если да, тоupdate
,в противном случаеinsert
. Кроме того, такие среды сохранения, как Hibernate, автоматически обеспечиваютsaveOrUpate()
Методы можно использовать непосредственно для сохранения совокупных корней.
Для функции запроса вполне разумно реализовать запрос в репозитории, однако развитие проекта может привести к тому, что репозиторий будет заполнен большим количеством кодов запросов, что, по-видимому, скрывает первоначальную цель репозитория. . На самом деле, операция чтения и операция записи в DDD — это два очень разных процесса, и автор предлагает реализовать их по отдельности, насколько это возможно, чтобы функция запроса была отделена от репозитория, о чем я подробно расскажу. ниже.
В этом примере мы используем техническую реализацию SpringJdbcTemplate
и сохранение формата JSONOrder
Совокупный корень, фактически репозиторий, не привязан к определенному механизму сохранения.Функциональный «интерфейс», предоставляемый абстрактным репозиторием, всегда должен предоставлять совокупный корневой объект модели домена, точно так же, как «дом совокупного корня». ".
Итак, давайте проведем обзор. Выше мы взяли в качестве примера бизнес-требование «обновить количество продуктов в заказе» и рассказали о службах приложений, корнях агрегации и библиотеках ресурсов. требование отражает обработку DDD.Наиболее распространенные и типичные формы бизнес-требований:
В качестве общего координатора служба приложений сначала получает совокупный корень через репозиторий, затем вызывает бизнес-метод в совокупном корне и, наконец, снова вызывает репозиторий, чтобы сохранить совокупный корень.
Блок-схема выглядит следующим образом:
Столп Творения - Фабрика
После небольшого уточнения мы знаем, что операция записи в программном обеспечении либо изменяет существующие данные, либо создает новые данные. Для первого ответ, данный DDD, был упомянут выше, Далее мы поговорим о том, как создать новый совокупный корень в DDD.
Совокупные корни обычно создаются черезЗаводской режимComplete, с одной стороны, вы можете пользоваться преимуществами самого шаблона factory, с другой стороны, Factory в DDD также имеет эффект отображения «логики создания совокупного корня».
Процесс создания корня агрегата может быть простым или сложным, иногда конструктор вызывается напрямую, а иногда возникает сложный процесс построения, например, необходимость вызова других систем для получения данных. Вообще говоря, есть два способа реализовать Factory:
- Реализуйте метод Factory непосредственно в корне агрегата, который часто используется для простых процессов создания.
- Независимый класс Factory для процесса создания с определенной сложностью или логика создания не подходит для размещения в корне агрегата
Давайте сначала продемонстрируем простой метод Factory, в примере системы заказов бизнес-прецедент — «создать продукт»:
Создайте продукт с атрибутами, включая имя, описание и цену, а ProductId — это UUID.
существуетProduct
Реализовать фабричный метод в классеcreate()
:
public static Product create(String name, String description, BigDecimal price) {
return new Product(name, description, price);
}
private Product(String name, String description, BigDecimal price) {
this.id = ProductId.newProductId();
this.name = name;
this.description = description;
this.price = price;
this.createdAt = Instant.now();
}
здесь,Product
серединаcreate()
Метод не содержит логики создания, а напрямую делегирует процесс созданияProduct
конструктор. ты можешь подумать этоcreate()
Этот метод несколько избыточен, но первоначальная цель этого подхода остается прежней: мы хотим выделить логику создания совокупного корня. Конструктор сам по себе является очень технической вещью, каждый раз, когда он связан с созданием нового объекта в памяти компьютера, вам нужно использовать конструктор, независимо от того, изначальной причиной создания являются потребности бизнеса, или загрузка из базы данных, или десериализация из данных JSON. . Поэтому часто в программе присутствует несколько конструкторов для разных сценариев.Чтобы отличить создание бизнеса от технического творчества, мы ввелиcreate()
Методы используются для представления процесса создания бизнеса.
Фабрика, созданная с помощью «Создать продукт», очень проста. Давайте рассмотрим другой пример: «Создать заказ»:
Создайте Заказ, включая выбранный пользователем Товар и его количество, а OrderId необходимо получить, вызвав сторонний OrderIdGenerator
здесьOrderIdGenerator
Это объект со свойствами службы (т. е. службы домена ниже), и в DDD совокупный корень обычно не относится к другим классам службы. Кроме того, вызов OrderIdGenerator для создания идентификатора должен быть бизнес-подробностью.Как упоминалось ранее, эту деталь не следует размещать в ApplicationService. На данный момент создание Заказа может быть выполнено через класс Factory:
@Component
public class OrderFactory {
private final OrderIdGenerator idGenerator;
public OrderFactory(OrderIdGenerator idGenerator) {
this.idGenerator = idGenerator;
}
public Order create(List<OrderItem> items, Address address) {
OrderId orderId = idGenerator.generate();
return Order.create(orderId, items, address);
}
}
Необходимый компромисс — доменные службы
Как мы упоминали ранее, совокупный корень является основным носителем бизнес-логики, то есть код реализации бизнес-логики должен быть размещен в пределах совокупного корня или границы совокупного корня, насколько это возможно. Но иногда некоторая бизнес-логика не подходит для размещения в корне агрегата, например, в предыдущем примере.OrderIdGenerator
Вот и все, в этой «последней ситуации» мы вводим доменные службы (Domain Service). Давайте сначала рассмотрим один пример.Существуют следующие варианты использования оплаты заказа в бизнесе:
Завершите оплату Заказа через платежный шлюз OrderPaymentService.
существуетOrderApplicationService
, вызвать службу домена напрямуюOrderPaymentService
:
@Transactional
public void pay(String id, PayOrderCommand command) {
Order order = orderRepository.byId(orderId(id));
orderPaymentService.pay(order, command.getPaidPrice());
orderRepository.save(order);
}
затем реализоватьOrderPaymentService
:
public void pay(Order order, BigDecimal paidPrice) {
order.pay(paidPrice);
paymentProxy.pay(order.getId(), paidPrice);
}
здесьPaymentProxy
иOrderIdGenerator
аналогичный, не подходит дляOrder
середина. Видно, что вOrderApplicationService
, мы не вызываем напрямуюOrder
деловой метод вOrderPaymentService.pay()
, затем вOrderPaymentService.pay()
Завершите вызов платежного шлюза вPaymentProxy.pay()
Такие деловые подробности.
На этом этапе давайте взглянем на классы службы, которые мы обычно пишем. На самом деле, эти классы службы объединяют ApplicationService и DomainService в DDD. Например, OrderService в разделе «Реализация на основе модели службы + анемии». В DDD ApplicationService и DomainService — это два совершенно разных понятия, первое — обязательный компонент DDD, а второе — просто компромиссный результат, поэтому чем меньше DomainService в программе, тем лучше.
Командный объект
Вообще говоря, операция записи в DDD не требует возврата данных клиенту.В некоторых случаях (например, при создании нового корня агрегата) может быть возвращен идентификатор корня агрегата, что означает, что метод операции записи в ApplicationService или совокупный корень обычно возвращаютvoid
Вот и все. Например, дляOrderApplicationService
, сигнатура каждого метода выглядит следующим образом:
public OrderId createOrder(CreateOrderCommand command) ;
public void changeProductCount(String id, ChangeProductCountCommand command) ;
public void pay(String id, PayOrderCommand command) ;
public void changeAddressDetail(String id, String detail) ;
Как видите, в большинстве случаев мы используем суффикс какCommand
Объект передается в ApplicationService, напримерCreateOrderCommand
иChangeProductCountCommand
. Команда означает команду, то есть операция записи представляет собой командную операцию, инициированную извне в модель предметной области. На самом деле, с технической точки зрения, объект Command — это просто тип объекта DTO, который инкапсулирует данные запроса, отправленные клиентом. Все операции записи, поступающие в Контроллер, должны быть упакованы Командой.В случае, когда Команда относительно проста (например, всего 1-2 поля), Контроллер может распаковать Команду и передать данные непосредственно в Служба приложений, напримерchangeAddressDetail()
Это тот случай, когда в команде много полей данных, объект команды можно напрямую передать в службу приложений. Конечно, это не тот принцип, который нужно строго соблюдать в DDD, например, несмотря на простоту и сложность Команды, не составляет большой проблемы равномерно передать все Команды из Контроллера в ApplicationService. больше выбора привычек кодирования. Но необходимо подчеркнуть одну вещь, то есть вышеупомянутый «ApplicationService должен принимать примитивные типы данных вместо объектов в модели предметной области», а это означает, что объект Command также должен содержать примитивные типы данных.
Другое преимущество унифицированного использования объекта Command состоит в том, что мы можем найти все суффиксы какCommand
объект, вы можете получить обзор бизнес-функций, предоставляемых программной системой.
Подводя итог поэтапно, выше мы в основном обсуждали реализацию программной «операции записи» в DDD, и говорили о 3-х сценариях, а именно:
- Выполнение бизнес-запросов через совокупные корни
- Завершите создание совокупных корней через Factory
- Выполнение бизнес-запросов через DomainService
Приведенные выше три сценария в общих чертах охватывают основные аспекты DDD для выполнения операций делового письма и могут быть суммированы в трех предложениях: создание совокупного корня выполняется через Фабрику, бизнес-логика предпочтительно завершается в границах совокупного корня; бизнес-логика, которая ненадлежащим образом помещена в общий корень, — это только рассмотрите возможность размещения в DomainService.
Операции чтения в DDD
Модель чтения и модель записи в программном обеспечении очень разные.Бизнес-логика, о которой мы обычно говорим, больше связана с тем, на что нам нужно обратить внимание во время операции записи, в то время как операция чтения больше о том, как вернуть соответствующее значение в клиент.отображение данных.
В операции записи DDD нужно строго следовать структуре «служба приложения->корень агрегации->библиотека ресурсов», а в операции чтения использование той же структуры, что и операция записи, иногда не только не приносит пользы, но и делает Весь процесс становится утомительным. Вот 3 способа чтения операций:
- Операции чтения на основе модели домена
- Операции чтения на основе модели данных
- CQRS
Прежде всего, независимо от того, какой метод операции чтения, вы должны следовать принципу: объекты в модели предметной области не могут быть напрямую возвращены клиенту, потому что внутренняя часть модели предметной области открыта внешнему миру, а модификация модели предметной области напрямую повлияет на клиента. Поэтому в DDD мы обычно создаем соответствующую модель операции чтения для представления данных. В операции записи мы используем суффикс Command для объединения запрошенных данных. В операции чтения мы используем суффикс представления для объединения данных. Представление здесь также является буквой «R» в REST.
Операции чтения на основе модели домена
Этот метод сочетает в себе модель чтения и модель записи, сначала получает модель предметной области через библиотеку ресурсов, а затем преобразует ее в объект представления, что также является методом, который в настоящее время широко используется.Реализация выглядит следующим образом:
@Transactional(readOnly = true)
public OrderRepresentation byId(String id) {
Order order = orderRepository.byId(orderId(id));
return orderRepresentationService.toRepresentation(order);
}
давайте пройдемсяorderRepository.byId()
получитьOrder
Агрегируйте корневой объект, затем вызовитеorderRepresentationService.toRepresentation()
будетOrder
Преобразовать в объект презентацииOrderRepresentation
,OrderRepresentationService.toRepresentation()
Реализация выглядит следующим образом:
public OrderRepresentation toRepresentation(Order order) {
List<OrderItemRepresentation> itemRepresentations = order.getItems().stream()
.map(orderItem -> new OrderItemRepresentation(orderItem.getProductId().toString(),
orderItem.getCount(),
orderItem.getItemPrice()))
.collect(Collectors.toList());
return new OrderRepresentation(order.getId().toString(),
itemRepresentations,
order.getTotalPrice(),
order.getStatus(),
order.getCreatedAt());
}
Преимущество этого метода в том, что он очень прост, и нет необходимости создавать новый механизм чтения данных, просто используйте репозиторий для непосредственного чтения данных. Однако недостатки тоже очевидны: во-первых, операция чтения полностью привязана к граничному разделу корня агрегата, например, если клиенту необходимо получитьOrder
и что в нем содержитсяProduct
, то нам нужно объединитьOrder
Совокупный корень иProduct
Корень агрегата загружается в память, а затем преобразуется.Этот метод громоздкий и неэффективный.Во-вторых, при операции чтения обычно необходимо возвращать данные на основе различных условий запроса, например, черезOrder
дата запроса или поProduct
В результате в репозитории обрабатывается слишком много логики запросов, которая становится все более и более сложной и постепенно отклоняется от обязанностей, которые должен взять на себя репозиторий.
Операции чтения на основе модели данных
Этот метод обходит библиотеку ресурсов и агрегацию и напрямую считывает данные, необходимые клиенту, из базы данных.В настоящее время операция записи и операция чтения совместно используют только базу данных. Например, для интерфейса «Получить список товаров» через специальныйProductRepresentationService
Чтение данных непосредственно из базы данных:
@Transactional(readOnly = true)
public PagedResource<ProductSummaryRepresentation> listProducts(int pageIndex, int pageSize) {
MapSqlParameterSource parameters = new MapSqlParameterSource();
parameters.addValue("limit", pageSize);
parameters.addValue("offset", (pageIndex - 1) * pageSize);
List<ProductSummaryRepresentation> products = jdbcTemplate.query(SELECT_SQL, parameters,
(rs, rowNum) -> new ProductSummaryRepresentation(rs.getString("ID"),
rs.getString("NAME"),
rs.getBigDecimal("PRICE")));
int total = jdbcTemplate.queryForObject(COUNT_SQL, newHashMap(), Integer.class);
return PagedResource.of(total, pageIndex, products);
}
Затем вернитесь прямо в контроллер:
@GetMapping
public PagedResource<ProductSummaryRepresentation> pagedProducts(@RequestParam(required = false, defaultValue = "1") int pageIndex,
@RequestParam(required = false, defaultValue = "10") int pageSize) {
return productRepresentationService.listProducts(pageIndex, pageSize);
}
Видно, что реальный процесс не используетсяProductRepository
иProduct
, а непосредственно создавать данные, полученные с помощью SQL, какProductSummaryRepresentation
объект.
Преимущество этого метода в том, что процесс операции чтения не ограничивается моделью предметной области, а может напрямую получать требуемые данные исходя из требований самой операции чтения.С одной стороны, весь процесс упрощается, а с другой стороны, производительность значительно улучшается. Однако, поскольку операции чтения и записи совместно используют базу данных, а база данных в настоящее время в основном создается в соответствии со структурой сводного корня, операции чтения по-прежнему ограничены моделью данных операций записи. Однако этот метод является хорошим компромиссом, и корпорация Майкрософт также поддерживает этот метод.официальный сайт майкрософт.
CQRS
CQRS(Command Query Responsibility Segregation), то есть разделение обязанностей команд-запросов.Команду здесь можно понимать как операцию записи, а запрос — как операцию чтения. В отличие от «операций чтения на основе модели данных», в операциях записи и чтения CQRS используются разные базы данных, и данные синхронизируются из базы данных модели записи в базу данных модели чтения, обычно черезСобытия доменаИнформация о синхронизированных изменениях в виде .
Таким образом, операция чтения может независимо проектировать структуру данных в соответствии со своими потребностями, не ограничиваясь структурой данных модели записи. CQRS — это большая тема, выходящая за рамки этой статьи, и читатели могут исследовать ее самостоятельно.
Пока операцию чтения в DDD можно условно разделить на 3 реализации:
Суммировать
В этой статье в основном представлены концепции службы приложений, агрегации, библиотеки ресурсов и фабрики в DDD и связанных с ними методов кодирования, а затем основное внимание уделяется реализации программных операций чтения и записи в DDD.Три сценария операций записи:
- Выполнение бизнес-запросов через сводные корни, что является типичным способом, которым DDD выполняет бизнес-запросы.
- Создание совокупных корней завершается с помощью Factory, которая используется для создания совокупных корней.
- Выполняйте бизнес-запросы через DomainService и рассматривайте возможность размещения бизнеса в DomainService только тогда, когда нецелесообразно размещать бизнес в совокупном корне.
Для операций чтения также даются три метода:
- Операции чтения на основе модели предметной области (операции чтения и записи объединены, не рекомендуется)
- Операции чтения на основе модели данных (рекомендуется миновать совокупный корень и репозиторий, возвращать данные напрямую)
- CQRS (операции чтения и записи используют разные базы данных)
Вышеупомянутое «3 чтения и 3 письма» в основном покрывает потребности программистов для ежедневной разработки бизнес-функций.Выходит, что DDD так прост, не так ли?