Влад Хононов

Что такое предметно-ориентированное проектирование?


(What is Domain-Driven Design?)

Глава 5
Паттерны реализации бизнес-логики
Бизнес-логика — самая важная часть программного обеспечения. Это главная причина, по которой программное обеспечение вообще разрабатывается. Интерфейс пользователя системы может быть привлекательным, база данных — стремительной и масштабируемой, но если программное обеспечение бесполезно для бизнеса, оно не более чем технологическая демонстрация.

В первой части этой книги мы видели, насколько важно, чтобы все заинтересованные стороны общались на едином языке и имели общее понимание области задач. Исходный код системы также должен «говорить» на этом языке и разрабатываться в соответствии с общей моделью предметной области. Но как это реализовать?

Как мы узнали из главы 2, не все предметные подобласти бизнеса одинаковы. Разные подобласти имеют разный уровень стратегической важности и сложности. В этой главе мы рассмотрим четыре различных способа реализации бизнес-логики в коде: транзакционный сценарий, активная запись, модель предметной области и модель предметной области, основанная на событиях. Каждый паттерн соответствует разной степени сложности в предметной области.
Транзакционный сценарий (Transaction Script)
Этот паттерн хорошо подходит для самых простых областей задач, где бизнес-логика напоминает операции извлечения, преобразования и загрузки (ETL) — то есть каждая операция извлекает данные из источника, применяет логику преобразования для конвертирования их в другую форму и загружает результат в целевое хранилище. Этот процесс показан на рисунке 5-1.

Рисунок 5-1. Извлечение-преобразование-загрузка потока данных

Соответственно, реализация этого паттерна проста. Он организует бизнес-логику по процедурам, где каждая процедура выполняет запрос из публичного интерфейса системы.

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

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

Приведем пример транзакционного сценария, который преобразует пакеты файлов JSON в файлы XML:
DB.StartTransaction();
 var job = DB.LoadNextJob();
 var json = LoadFile(source);
 var xml = ConvertJsonToXml(json);
 WriteFile(destination, xml.ToString();
 DB.MarkJobAsCompleted(job);
 DB.Commit()
Паттерн сценария естественным образом подходит для поддерживающих предметных подобластей, где бизнес-логика, по определению, проста. Этот шаблон не следует использовать для основных предметных подобластей, так как он не справится со сложностью бизнес-логики.
Активная запись (Active Record)
Как и предыдущий паттерн, активная запись также подходит для областей с простой бизнес-логикой. Однако здесь бизнес-логика может оперировать более сложными структурами данных. Например, вместо плоских записей у нас могут быть более сложные объектные деревья и иерархии, как показано на рисунке 5-2.

Рисунок 5-2. Более сложная модель данных

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

Следовательно, этот паттерн использует специальные объекты для представления сложных структур данных: активные записи. Помимо структуры данных, эти объекты также реализуют методы доступа к данным для создания, чтения, обновления и удаления записей — так называемые CRUD-операции. В результате активные объекты записей зависят от объектно-реляционного отображения (ORM) или какого-либо другого фреймворка доступа к данным. Название паттерна происходит из того факта, что каждая структура данных «активна», то есть реализует логику доступа к данным.

Как и в случае с паттерном транзакционного сценария, бизнес-логика системы организуется процедурами. Разница между этими двумя паттернами заключается в том, что в данном случае, вместо доступа к базе данных напрямую, процедуры управляют объектами активных записей:
public class CreateUser {
  public void Execute(userDetails) {
    try {
      DB.StartTransaction();

      var user = new User();
      user.Name = userDetails.Name;
      user.Email = userDetails.Email;
      user.Save();

      DB.Commit();
    } catch {
      DB.Rollback();
throw;
    }
  }
}
Этот паттерн может поддерживать только относительно простую бизнес-логику — операции CRUD или проверку пользовательского ввода. Следовательно, паттерн активной записи подходит для поддерживающих предметных подобластей.
Модель предметной области (Domain Model)
Паттерн модели предметной области предназначен для решения задач сложной бизнес-логики. Здесь, вместо интерфейсов CRUD, мы имеем дело со сложными бизнес-правилами и инвариантами, которые должны быть защищены.

Как и паттерны сценария транзакций и активной записи, паттерн модели предметной области был представлен в книге Мартина Фаулера «Шаблоны корпоративных приложений» (Addison-Wesley). Однако реализация этого паттерна была подробно описана только год спустя Эриком Эвансом в его книге «Предметно-ориентированное проектирование (DDD). Структуризация сложных программных систем» (Addison-Wesley) как инструмент для реализации основных предметных подобластей.
Реализация
Как определено у Мартина Фаулера, модель предметной области — это объектная модель, которая включает в себя как поведение, так и данные. Строительными блоками этой объектной модели являются тактические паттерны предметно-ориентированного проектирования: агрегаты, объекты-значения, события предметной области и так далее.

Все эти паттерны имеют общую черту: приоритет бизнес-логики. Давайте рассмотрим, как модель предметной области учитывает различные аспекты проектирования.
Сложность
Бизнес-логика предметной области уже сама по себе сложна, поэтому объекты, моделирующие ее, не должны вносить дополнительных непреднамеренных сложностей. Модель не должна зависеть от инфраструктурных или технических аспектов. Это ограничение предписывает объектам модели быть простыми.
Единый язык
Акцент на бизнес-логику, а не технические аспекты позволяет объектам модели предметной области использовать терминологию единого языка ограниченного контекста. Другими словами, этот паттерн позволяет коду «говорить» на едином языке и соответствовать ментальным моделям экспертов предметной области.
Строительные блоки
Давайте рассмотрим два строительных блока модели предметной области (тактические паттерны), предложенные предметно-ориентированным проектированием: объект-значение и агрегаты.
Объект-значение (Value Object)
Объект-значение — это объект, который можно идентифицировать по его значениям. Пример такого объекта:
class Color {
  int red;
  int green;
  int blue
}
Изменение значения одного из полей приведёт к новому цвету. У двух разных цветов не может быть одинаковых значений. Также два экземпляра одного и того же цвета должны иметь одинаковые значения. Таким образом, для идентификации цветов не требуется явного идентификатора.
Реализация
Так как изменение любого из полей объекта-значения приводит к новому значению, объекты-значения реализованы как неизменяемые объекты.

Когда выполняется действие, результатом которого является новое значение, оно не изменяет исходный экземпляр, а создает и возвращает новый:
class Color {
  Color mixWith(Color other)
    …
    return new Color(...);
  }
}
Банальным примером объекта-значения является объект String в стеках Java и .NET. Он неизменяемый и все его операции приводят к созданию нового экземпляра строки.
Агрегат (Aggregate)
Агрегат — это сущность, представляющая иерархию объектов. В отличие от объекта-значения, сущность нельзя идентифицировать только по его значению. Это означает, что для идентификации каждого агрегата требуется поле идентификации.
В качестве примера рассмотрим двух людей, разделяющих одно и то же имя. Это не делает их одним и тем же человеком. Следовательно, нам нужно специальное поле для идентификации людей:
class Person {
  Guid Id;
  String FirstName;
  String LastName;
}
Также в отличие от объектов-значений, невозможность идентифицировать агрегаты по их значениям делает их состояние изменяемым. Рассмотрим последствия этого и некоторые другие свойства агрегатов.
Согласованность
Так как агрегат может быть изменён со временем, важно обеспечить согласованность его состояния. Для обеспечения согласованности агрегатный паттерн устанавливает четкую границу между агрегатом и его внешним контекстом. Только бизнес-логика агрегата может изменять его состояние. Все процессы или объекты, находящиеся за пределами агрегата, могут только читать его состояние или выполнять его публичные методы.

Публичные методы агрегата отвечают за валидацию ввода и соблюдение всех бизнес-правил и инвариантов. Это строгая граница также обеспечивает то, что вся бизнес-логика, связанная с агрегатом, реализуется в одном месте — в самом агрегате.

Некоторые объекты должны изменяться вместе.
Цель паттерна агрегата — обеспечить согласованность его состояния. Все объекты внутри агрегата могут изменяться только самим агрегатом. Ни один внешний процесс или слой не может манипулировать значениями агрегата напрямую, а только через его публичный интерфейс.
Граница транзакции
Так как состояние агрегатов может быть изменено только собственной бизнес-логикой, граница агрегата также является границей транзакции. Все изменения состояния агрегата должны быть зафиксированы транзакционно как одна атомарная операция.

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

Кажется, что это накладывает ограничение на моделирование. Что делать, если нам нужно изменить несколько агрегатов в одной транзакции? Давайте посмотрим, как паттерн решает такие ситуации.
Иерархия объектов
Существуют бизнес-сценарии, при которых несколько объектов должны иметь общую транзакционную границу — например, когда оба объекта могут быть изменены одновременно или бизнес-правила одного объекта зависят от состояния другого.

Предметно-ориентированное проектирование предписывает, что дизайн системы должен определяться её бизнес-областью. Агрегаты — не исключение.

Агрегат — это не плоская запись. Это документ — иерархия связанных объектов, как показано на рисунке 5-3.

Рисунок 5-3. Агрегат как иерархия объектов

Другие агрегаты и объекты-значения — иерархия объектов — могут принадлежать одному агрегату, если их объединяет бизнес-логика предметной области.

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

Согласованность данных может служить удобным правилом для проектирования границ агрегатов. Внутри границ агрегата должна находиться только строго согласованная для реализации бизнес-логики агрегата информация. Объекты, которые могут быть согласованными нестрого, не должны принадлежать агрегату и могут быть обозначены по их ID, как показано на рисунке 5-4.

Рисунок 5-4. Агрегат как граница согласованности

Правило хорошего тона — сохранять агрегаты небольшими, включая в него только строго согласованные в пределах бизнес-области объекты.
Корень агрегата
Мы уже знаем, что состояние агрегата можно изменить, выполнив один из его публичных методов. Поскольку агрегат представляет иерархию объектов, только один из них должен быть определен как публичный интерфейс агрегата — корень. Это показано на рисунке 5-5.

Рисунок 5-5. Корень агрегата

Помимо корня агрегата, для взаимодействия с внешним миром существует ещё один механизм — события предметной области (domain events).
События предметной области
Событие предметной области — это сообщение, описывающее произошедшее значимое событие. Например:

  • Заказ оплачен
  • Склад пополнен
  • Опубликована рекламная кампания

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

События предметной области являются частью публичного интерфейса агрегата. Агрегат публикует свои события. Другие процессы, агрегаты или даже внешние системы могут следить за этими событиями и выполнять свою собственную логику в ответ на события предметной области, как показано на рисунке 5-6.

Рисунок 5-6. Поток публикации событий предметной области

Другие строительные блоки
Агрегаты и объекты-значения — лишь два из многих тактических паттернов объектно-ориентированного проектирования. Другие существующие паттерны: служба предметной области, репозиторий, сага и фабрика.
Модель предметной области,
основанная на событиях
(Event-Sourced Domain Model)
Паттерн модели предметной области, основанной на событиях базируется на том же принципе, что и паттерн модели предметной области. Бизнес-логика также сложна и принадлежит к основной предметной подобласти. Более того, он использует те же тактические паттерны, что и модель предметной области: объект-значение, сущности, агрегаты и корни агрегатов. Разница между этими паттернами реализации заключается в способе хранения состояния агрегатов. Паттерн модели предметной области, основанной на событиях использует источник событий для управления состояниями агрегатов.
Источник событий
Понятие источника событий наиболее легко объяснить на примере. Допустим, мы работаем над CRM-системой и у нас есть следующая запись о клиенте (рисунок 5-7).

Рисунок 5-7. Модель, основанная на состоянии

В системе, основанной на состоянии, представленные значения — это всё, что мы знаем об этом клиенте. Мы не знаем, как клиент туда попал. Купил ли он продукт сразу или прошел долгий путь продаж? Эта информация отсутствует. Мы знаем только текущее состояние Уильяма.

Паттерн модели предметной области, основанной на событиях использует события предметной области для введения измерения времени в модель. Каждое изменение состояния системы должно быть выражено и записано как событие предметной области. Рассмотрим события предметной области на рисунке 5-8.

Рисунок 5-8. Представление, основанное на событиях

Показанные здесь события предметной области описывают путь клиента:

  1. Клиент был инициализирован в системе.
  2. Продавец связался с клиентом, но тот попросил перезвонить позже по другому номеру телефона.
  3. С клиентом снова связались и на этот раз он согласился купить продукт.
Состояние клиента, которое мы видели ранее, можно легко получить из этих событий предметной области.
Источник истины
Чтобы событийное хранилище работало, все изменения состояния объекта должны быть представлены и сохранены как события предметной области. Эти события становятся источником истины системы (отсюда и название паттерна). Этот процесс показан на рисунке 5-9.

Рисунок 5-9. Агрегат, основанный на событиях

База данных, которая хранит события предметной области системы, является единственным строго согласованным хранилищем — источником истины системы.

Каждая операция над агрегатом, использующим источник событий, следует сценарию:

  1. Загрузить события предметной области агрегата.
  2. Восстановить представление состояния.
  3. Выполнить бизнес-логику и создать новые события предметной области.
  4. Зафиксировать новые события предметной области в базе данных.

В сущности, паттерн моделирования событий не делает ничего нового. Финансовая отрасль использует события для представления изменений в учётной книге. Учётная книга — это журнал только для добавления, который документирует транзакции. Текущее состояние (например, баланс счета) всегда можно вывести, «проецируя» записи в учётной книге.

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

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

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

Журнал аудита
Сохранённые события предметной области представляют собой строго согласованный журнал аудита всего, что произошло с состояниями агрегатов. Юридические законы обязывают некоторые предметные области внедрять такие журналы. Источник событий позволяет делать это сразу.

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

Транзакционный сценарий
Этот паттерн организует операции системы как простые, прямолинейные процедурные сценарии. Процедуры обеспечивают транзакционную природу каждой операции — либо она успешно завершается, либо нет. Паттерн транзакционного скрипта подходит для поддержки предметных подобластей с бизнес-логикой, напоминающей простые операции типа ETL.

Активная запись
Когда бизнес-логика проста, но оперирует сложными структурами данных, можно реализовать эти структуры данных в виде активных записей. Объект активной записи представляет собой структуру данных, предоставляющую простые методы доступа к данным CRUD.

Модель предметной области
Если бизнес-логика сложна, реализуйте её, используя тактические шаблоны объектно-ориентированного проектирования: объекты-значения, агрегаты и корни агрегатов.

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