Декомпозиция систем по ограниченным контекстам DDD — глубокое погружение
"Отдайте этот функционал в другую системы - он относится к ним" - ворчал мой собеседник. Ему с пылом отвечали: "Так быть не должно. Мы сами должны его сделать!" Спор грозил затянуться до вечера. Ни одна из сторон не могла привести ни одного настоящего аргумента, почему новый функционал нужно поместить в ту или иную автоматизированную систему.
Проблема была в том, что никто не понимал как правильно делить системы на части и по каким признакам включать в них новые модули. У собеседников не было никакой единой простой методики.
Но методика на самом деле есть, и весьма неплохая. Называется она Предметно Ориентированным Дизайном (Domain Driven Design, DDD). С помощью DDD деление большой системы на (микро)сервисы становится простым и понятным.
Центральным понятием теории DDD является "ограниченный контекст" (bounded context). К сожалению, даже прочитав книги Эванса и Вернона от корки до корки, остаётся совершенно неясным, что ж на самом деле обозначает этот термин и как его правильно применять на практике.
Автору статьи после прочтения Синей (Эванса) и Красной (Вернона) книг пришлось перелопатить гору дополнительной литература и пересмотреть десятки, если не сотни, англоязычных видео, чтобы в подробностях разобраться в вопросе и составить для себя чёткое понимание алгоритма, заложенного в основу DDD.
Откуда ноги растут
Термин "ограниченный контекст" введён в книгах как центральное звено стратегического проектирования - это когда проектирование системы производят сверху-вниз. Делается это для того, чтобы иметь чёткое видение о том, как правильно разделять (декомпозировать) систему на сервисы, почему её так надо разделять и как действовать в будущем, когда потребуется что-то доработать.
Стратегия - это способ достижения цели. Поэтому стратегическое проектирование - это проектирование "начиная от цели", когда строящаяся система делится на части не от балды, а строго в соответствии со стратегией - способом достижения совершенно конкретной цели.
Это и понятно, ведь системы в принципе создаются для того, чтобы достигать целей (решать различные бизнес-задачи). Решения этих задач (стратегии достижения целей) обычно строятся на основе используемых в задачах бизнес-терминов.
Поэтому можно сказать, что всё начинается с бизнес-терминов.
Пространство задач и пространство решений
Бизнес-термины объединяются в поддомены, которые в свою очередь содержат ограниченные контексты. Давайте сначала рассмотрим понятие поддомена, а потом поймем как в него встраивается ограниченный контекст.
Системы решают множество задач, поэтому совокупность всех решаемых системой задач в DDD принято называть пространством задач (problem space), а совокупность всех решений этих задач - пространством решений (solution space).
Набор применяемых решений конкретной задачи называется поддоменом (subdomain/подобласть). Все поддомены вместе образуют один большой домен решаемой задачи (domain/область).
Рассмотрим конкретный пример из практики.
Компания оказывает клиентам услугу пополнения офисов канцелярскими товарами.
Это цель компании, и для её реализации решено сделать ИТ-систему.
Назначение системы - решить задачу снабжения офисов клиентов канцелярией.
Каким образом можно решить поставленную задачу (снабжения офисов)? Очевидно, что для организации снабжения понадобится:
Привлечь клиента
Заключить с клиентом договор о снабжении
Определить потребность в снабжении - понять какому офису какие товары возить, с какой периодичность и как определять необходимое количество товара для поставки
Закупить необходимый запас товаров и разместить у себя на складе
Подготовить поставку - упаковать необходимое количество товара в грузовые места, обклеить этикетами и сгруппировать по снабжаемым офисам
Заказать транспорт
Доставить до конечного получателя
Произвести взаиморасчеты с клиентом
По сути, в домене задачи "услуга пополнения офисов" пространства решений (поддомены) довольно хорошо очерчиваются:
- Привлечение клиентов
- Заключение договора
- Определение потребности в снабжении
- Закупка товарного запаса
- Подготовка поставки
- Заказ транспорта
- Доставка
- Взаиморасчёты
На данном этапе не следует путать поддомены с модулями системы. Речь о другом. Речь пока что идёт об определении всех способов решения основной задачи.
Поддомен - это пространство решений конкретной задачи (домена).
Пространство - значит, что может быть множество способов решения. Например, для поддомена Взаиморасчетов можно закупить систему 1С и с помощью неё полностью закрыть всё, что связано со взаиморасчётами. А в других случаях готовой системы может не быть, и её придётся делать с нуля. Или наоборот, может быть несколько систем, которые в совокупности решат свою часть большой задачи.
Рассмотрим пример с поддоменом Привлечения клиентов.
В качестве решений для привлечения клиентов обычно используют лэндинги, собирающие контакты и помещающие их в CRM-систему. Задача CRM-аккумулировать в себе всю историю общения с клиентами по всем каналам коммуникаций. Также при привлечении клиентов часто складывают метрики в специальную систему для расчёта конверсий.
Т.е. поддомен Привлечения клиента состоит из трех решений: лэндинга, CRM-системы, Система сбора и анализа метрик.
А теперь, трам-пам-пам, возникает понятие ограниченного контекста.
Определения ограниченного контекста
Ограниченный контекст - это конкретное решение, реализация внутри поддомена, говорящее на своём собственном языке.
В одном поддомене может содержаться множество ограниченных контекстов (решений).
В поддомене Взаиморасчетов есть только один ограниченный контекст - это 1С. Она общается внутри себя на своём собственном языке, поэтому и выделяется в отдельный контекст.
В поддомене Привлечения клиентов аж целых три ограниченных контекста, каждый из которых внутри себя общается на своём собственном языке.
Залезть внутрь CRM и Системы метрик невозможно - они общаются внутри себя на своем собственном, недоступном никому больше языке. Поэтому и выделяются в отдельный контекст. А вот лэндинг - это собственное решение, которое делается с нуля. При разработке для него будет создана своя собственная модель данных со своими собственными бизнес-терминами.
Вот какое определение дают ограниченному контексту разные источники:
"Ограниченный контекст - это все аспекты, которые могут повлиять на опыт использования продукта"
"Ограниченный контекст - это лингвистическая граница вокруг сути модели"
"Ограниченный контекст - это граница, внутри которой модель имеет смысл"
"Ограниченный контекст - пространство, в котором существует только один Единый Язык, который используют и понимают все, кто входит в контекст, и не понимают те, кто в него не входит"
"Если в разных контекстах используется одинаковая сущность, то у неё должны быть разные свойства в этих контекстах, иначе разделение по контекстам сделано неверно"
"Всё, что находится внутри контекста, общается на одном языке, который не понимают другие контексты"
"Ограниченный контекст – это конкретное решение, реализация"
"Ограниченные контексты содержат агрегаты (неделимые объекты)"
"Одним ограниченным контекстом владеет одна команда"
"Ограниченный контекст должен позволить Вам поставлять сервис с большей частотой"
"Ограниченный контекст должен принадлежать только одному поддомену"
"Ограниченный контекст, по сути, это другое название для автономности - удаления зависимостей между разрабатываемыми системами и командами разработки. По другому его можно назвать контекстом автономности"
"Ограниченный контекст - это продуктовые и технические вещи, которые изменяются вместе по бизнесовым причинам"
Таким образом, ограниченный контекст - это конкретное решение (приложение) в рамках поддомена, внутри которого используется собственный язык (набор бизнес-терминов). Термины включают в себя все аспекты, влияющие на применение этого решения.
Т.е. по сути, ограниченный контекст - это ни что иное, как отдельное приложение/микросервис.
Единый язык ограниченных контекстов
Самым главным критерием выделения ограниченных контекстов является Единый язык.
Единый язык - это единый набор бизнес-терминов, применяемый внутри конкретного решения (реализации). Всё, что находится внутри этого решения обязано применять эти термины, и никак иначе.
Рассмотрим контекст Лэндингов из поддомена Привлечения клиентов. Какие термины применяются в лэндингах?
- Заявка с сайта
- Услуга
- Контактные данные
Вот как это выглядит на схеме:
Если переложить понятие единого языка на программирование, то он будет представлять из себя библиотеку классов с реализацией терминов поддомена предметной области, например, модель в ORM (Hibernate и проч).
Почему единый язык так важен?
Потому что он напрямую влияет на объем создаваемого решения.
Чем больше терминов используется в языке, тем больше распухает решение, а чем больше объем решения, тем быстрее оно стремится к монолиту и тем стремительнее пожирает компьютерные мощности.
Однако и мельчить здесь нельзя, потому что чем меньше размер решения, тем больше связанность между сервисами. Это тоже плохо.
При составлении единого языка рекомендуется применять две основных принципа:
принцип единственной ответственности
принцип максимальной близости
В этом случае применение единого языка позволяет найти баланс: создаваемое приложение с одной стороны получается небольшим, а с другой стороны полностью закрывает какую-то небольшую тему, да еще и получается независимым, поддерживая таким образом надежность системы в целом.
Почему единый язык применяется только внутри ограниченного контекста, а не глобально?
Если единый язык так важен, то почему бы не сделать его глобальным, применимым сразу для всех поддоменов предметной области? Т.е. взять и создать одну громадную библиотеку со всеми-всеми терминами предметной области?
Как насчет того, что таким образом Вы, по сути, получите сильно связанный монолит?
Объединять ли два ограниченных контекста в один?
Не всегда понятно, объединять ли термины в один контекст, либо же разносить на разные. Например, представьте, что у Вас получилось два разных набора бизнес-терминов с одинаковыми сущностями (отмечены красным).
Первый:
Второй:
Очевидно, что "красные" термины в обоих наборах полностью совпадают. Значит ли это, что оба этих языка следует объединить в один, единый язык и один ограниченный контекст?
В данной ситуации рекомендует пользоваться
Принципом единственной ответственности и
Принципом максимальной близости
Принцип единственной ответственности помогает
Принцип единственной ответственности - это паттерн (хорошая практика) проектирования классов, модулей и сервисов:
Изменения в модуле должны влиять на одного, и только на одного, Потребителя
Представьте, что Подготовка поставки и Заказ транспорта объединены в один модуль и имеют единую, связную (сильно связанную) структуру данных.
Тогда этим модулем будут пользоваться в двух целях: для подготовки поставки и для заказа транспорта. Т.е. у модуля будет два потребителя.
При внесении изменений, относящихся к подготовке поставки, будет обязательно страдать и часть, связанная с заказом транспорта.
Т.е. при возникновении ошибки в части Подготовки поставки остановится и часть по Заказу транспорта. Т.е. пострадают сразу оба Потребителя.
Если же разделить Подготовку поставки и Заказ транспорта по разным сервисам, то при возникновении проблемы в одном сервисе, другой этого даже и не почувствует. Т.е. влияние будет оказано только на одного Потребителя.
Т.е. принцип Единственной Ответственности делает систему в целом более устойчивой и автономной, а принцип максимальной близости дополняет её в плане анализа родственности данных.
И именно поэтому, кстати, ограниченный контекст называют контекстом автономности.
В нашем примере данные уже изначально разделены по разным поддоменам, т.е. они НЕ близки друг-другу.
В результате можно констатировать, что языки Подготовки поставки и Заказа транспорта объединять не следует.
Да, контексты будут связаны между собой через API или очереди, но языки у них при этом будут разными и независимыми.
Случаи, когда можно было бы сделать всё в одном контексте
Критерии принятия решения в этом случае:
Вы точно уверены, что второй второй контекст принадлежит первому? Действительно ли он является его частью и ВСЕГДА изменяется вместе
Ограниченный контекст - это продуктовые и технические вещи, которые изменяются вместе по бизнесовым причинам
Ограниченные контексты содержат агрегаты (неделимые объекты)
Одним ограниченным контекстом владеет одна команда
Ограниченный контекст должен позволить Вам поставлять сервис с большей частотой
Давайте рассмотрим пример, когда всё же стоило бы объединить несколько контекстов в один.
Все контексты в примере находятся в рамках одного поддомена Определение потребности в снабжении.
Команда разработки создала множество небольших отдельных микросервисов:
Реестр складов
Реестр клиентов
Структура подразделений компаний-клиентов
Поставляемая номенклатура
Т.к. ограниченные контексты - это конкретные решения (реализации) конкретных задач, то давайте выпишем как предполагается решать задачу определения потребности в снабжении.
Потребитель решения - Менеджер компании-поставщика:
Для конкретного клиента ввести список снабжаемых подразделений и их адреса.
Выбрать номенклатуру, доступную для заказа клиенту и каждому из его подразделений.
Указать склады, с которых будет осуществляться поставка номенклатуры для каждого из подразделений.
Вопрос в том, действительно ли нужно было разъединять все сущности в отдельные микросервисы? Или всё же все они относятся к одному и тому же приложению?
Для начала попробуем найти в созданных сервисах агрегаты. Агрегат - это неделимый объект.
Например, жёлтая машина с четырьмя колёсами. Цвет машины бессмысленен в отдельности от машины, также как и колёса. Вместо они образуют единое целое - машину. Если ломается одна из частей машины, то ломается, по сути, вся машина целиком.
Это и есть Агрегат. Изменения в любой из частей агрегата меняют весь Агрегат в целом.
Еще один пример Агрегата - Кредитная история. При добавлении нового платежа по кредиту или взятия нового кредита пересчитывается вся кредитная история целиком.
В нашем примере используется Реестр клиентов. Зададимся вопросом: А что входит в агрегат Клиента? Очевидно, что подразделения компании-клиента являются частью агрегата Клиента. При удалении одного из подразделений Клиента пересчитывается весь договор с Клиентом целиком.
Хорошо, ну а что насчет Поставляемой номенклатуры? Пересчитывается ли весь Клиент при изменении стоимости какой-либо позиции номенклатуры?
Нет, изменение стоимости номенклатуры не должно оказывать никакого влияния на Клиента, ведь все ранее сделанные заказы с этой позицией номенклатуры остаются неизменными, по старой цене, по которой были приобретены т.к. это история, состоявшийся факт, который не пересчитывается.
Поэтому Поставляемая номенклатура не является частью агрегата Клиент и может быть выделена в отдельный контекст. При этом история сделок Клиента, в которой фигурирует заказанная номенклатура, должна входить в агрегат и контекст Клиента.
Ровно такая же ситуация с Реестром складов. Он может быть выделен в отдельный контекст, однако история поставок клиенту со складов должна относится к контексту Клиента.
В итоге, не все сервисы из примера выделены верно. Их можно было было бы объединить.
Вывод
В статьей было объяснено понятие ограниченного контекста и показано его место в стратегическом проектировании сервисов предприятия. Приведены критерии выделения ограниченных контекстов и рассмотрены основные случаи, когда выделение сделано неверно.
Исходя из изложенного можно сделать вывод, что руководствуясь подходом DDD, ограниченными контекстами и применяя рассмотренные принципы, можно получить хорошо декомпозированную систему, обеспечивающую гибкое расширение, независимые релизы, устойчивую и надежную.
Вот по этому алгоритму можно довольно хорошо декомпозировать систему на составляющие, в т.ч. на микросервисы:
Определить перечень решаемых задач (пространство задач, домены)
Определить стратегию решения задач (поддомены)
В поддоменах определиться с реализациями/решениями
Проанализировать единый язык решений и распределить бизнес-термины в соответствии с агрегатами, принципом единственной ответсвенности и максимальной близости
Надеюсь, материалы этой статьи помогут Вам принять правильное решение и сделаю Вашу систему лучше. Буду рад Вашим комментариям.