"Отдайте этот функционал в другую системы - он относится к ним" - ворчал мой собеседник. Ему с пылом отвечали: "Так быть не должно. Мы сами должны его сделать!" Спор грозил затянуться до вечера. Ни одна из сторон не могла привести ни одного настоящего аргумента, почему новый функционал нужно поместить в ту или иную автоматизированную систему.

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

Но методика на самом деле есть, и весьма неплохая. Называется она Предметно Ориентированным Дизайном (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, ограниченными контекстами и применяя рассмотренные принципы, можно получить хорошо декомпозированную систему, обеспечивающую гибкое расширение, независимые релизы, устойчивую и надежную.

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

  • Определить перечень решаемых задач (пространство задач, домены)

  • Определить стратегию решения задач (поддомены)

  • В поддоменах определиться с реализациями/решениями

  • Проанализировать единый язык решений и распределить бизнес-термины в соответствии с агрегатами, принципом единственной ответсвенности и максимальной близости

Надеюсь, материалы этой статьи помогут Вам принять правильное решение и сделаю Вашу систему лучше. Буду рад Вашим комментариям.