Transcription
Приветствую вас на моём канале. Меня зовут Иван, и сегодня мы поговорим о DDD, или Domain-Driven Design, или объектно-ориентированное проектирование. Тема довольно большая, поэтому видео будет, скорее, обзорное. Я постараюсь рассказать, какие техники включает себя DDD, какую пользу вы можете извлечь из его техник, и постараюсь привести примеры из реальной практики.
Нужно сказать, что западные компании уже активно применяют этот подход, стараются внедрять на корпоративном уровне. У нас пока, конечно, запаздывает, но подвижки тоже есть. Как минимум, некоторые архитекторы уже стараются применять этот подход. Поэтому, если хотите стать архитектором или тимлидом, то вам эти знания точно нужны. Но и обычным разработчикам они тоже пригодятся, потому что DDD включает также и тактические шаблоны.
Не хочу делать долгое введение об истории создания DDD и прочее. Можете почитать на Википедии. Единственное, покажу несколько книг. Вот первая книга — это Эрика Эванса. Он первый предложил этот подход. Книга появилась ещё, по-моему, в 2003 году, но до сих пор актуальна. И также есть ещё две книги — это Вона Вернона. Тоже довольно-таки неплохие. Одна вот такая маленькая, одна побольше. Как говорят, чтобы познать DDD, нужно прочитать три книги: синюю, зелёную и красную. Я с этим не совсем согласен, но об этом позже. Название книг оставлю в описании.
Итак, начнём. Как раньше писали приложения? Как, например, работали так называемые веб-студии. Приходит заказчик, говорит: "У меня есть 100 объектов недвижимости, я сдаю их посуточно. Нужен сайт для автоматизации, что-то вроде Booking.com". И начинает на словах рассказывать своё техзадание: "Вот хочу, чтобы на главной странице можно было выбрать тип недвижимости, то есть это квартира, апартаменты или коттедж, цену, даты. Дальше нажимаешь поиск, и там показываются результаты с фоткой недвижимости и ценой. Чтобы была кнопка 'Забронировать'". Простое техзадание.
Его слушает какой-нибудь менеджер проектов, скорее всего, технарь, которого повысили, и он стал менеджером. Он говорит: "Окей. Ну, ты, конечно, в компьютерах не шаришь. Так это не делается. Сейчас всё расскажу. Что там говоришь типа недвижимости? Мы обычно называем это группой. Так. Дальше объекты недвижимости — это товары? Да, по сути. Правильно. Мы вот недавно сайт делали для мясника, один в один вообще как твой. Мы оттуда код скопируем, тебе вообще дешевле выйдет и быстрее. Так что никаких не движимых товаров. Кнопка 'Забронировать' — но это же покупка, правильно? Правильно. Вот и всё, у нас сходится. Есть товары, группы товаров, есть покупка. Сейчас всё сделаем".
Далее он передаёт всё это программистам и добавляет, что "вот прошлый сайт колбасный, возьмите за основу". Те смотрят: "Ну да, колбасный сайт идеально ложится. Единственное, у нас-то категория товаров называется не 'группа'". Ну ладно.
Что потом происходит? Клиент приходит через пару месяцев и говорит: "Хочу при бронировании продавать сопутствующие товары. Ну, типа, шампанское в номер. Ну и тут хочу, чтобы были категории товаров, то есть еда, алкоголь и так далее". И тут программисты: "Так, ну слово 'продукт' у нас занято. Назовём это ну, допустим, 'субпродукт'. Занято? Назовём это 'группой'". Уже представляете, какая путаница? Клиент говорит: "Допустим, добавьте ещё один тип недвижимости". Менеджер говорит программистам: "Добавить новую группу". А программисты сначала лезут в группы, а потом понимают, что это категория, а не группа. И чем дальше, тем будет хуже.
А представьте, если из этого проекта вырастет что-то большое, типа Booking.com или хотя бы Суточно.ру, а у него колбасный сайт в основе? Такой проект не выживет. Количество подобных проблем и недопонимания будет расти, а вместе с ним и количество ошибок. И в итоге добавление новой фичи или исправление старой будет занимать слишком большое количество времени. А потом ещё и менеджер уволится, который служил переводчиком между клиентом, программистами. И тогда всё будет полный "моя твоя не понимай".
И если вы думаете, что я утрирую, то вообще ни капли. Именно так раньше и работали эти самые веб-студии. Поэтому первое, на чём настаивает подход DDD, — это создание единого языка, или Ubiquitous Language по-английски. То есть программисты должны общаться с представителями бизнеса, ну, или, как говорят, с экспертами предметной области. И в ходе общения нужно подмечать, как эти самые эксперты называют те или иные объекты и действия. То есть, если эксперты часто повторяют фразу "отмена брони", значит, именно так вы потом и назовёте функцию в коде, или модуль, или что там нужно будет. Не "отменить забронированную недвижимость", не "разбронировать недвижимость", не "отменить покупку", а именно "отмена брони". Поверьте, потом это сэкономит вам кучу времени и нервов.
Как создаётся этот единый язык? Чёткой инструкции нет, но, по идее, это должно выглядеть как какой-то глоссарий, да, в котором будут как существительные, то есть какие-то объекты и субъекты, так и действия над ними. Допустим, недвижимость, забронировать недвижимость, отменить бронь, оплатить бронь, клиент, управляющий недвижимостью и так далее. Но, конечно, в таком списке можно легко запутаться и пользы от него не сильно много. Поэтому мы на одном из проектов любую фичу хорошо описывали в документах в сервисе Notion и выделяли какие-то фразы, которые скорее всего станут частью единого языка. Также в этих документах мы оставляли ссылки на событийные штурмы. Об этом тоже чуть позже поговорим. Событийные штурмы — это вообще кладезь фраз для единого языка. Также кладезь — это пользовательские сценарии, когда есть кому их писать, это вообще класс. Потом, когда мы начинали писать код для какой-то фичи, мы смотрели в эти документы и, как мы там обозвали какой-то объект или процесс, так и называли функцию, или класс, или переменную в коде. Если не хватало какого-то названия или мы сомневались в чём-то, то мы шли к нашим так называемым экспертам предметной области и спрашивали: "Допустим, а когда вы звоните клиенту, чтобы сказать, что его заказ подтверждён, как вы обычно это говорите?" И они отвечают, например: "Ну, мы говорим, что ваша бронь одобрена". Ага, значит, так мы пишем у себя в коде. И это давало свои плоды. Допустим, к нам пришёл новый продакт-менеджер, француз, и у него был не очень хороший английский, как и у нас, собственно. И он вообще был не программист, ничего в этом не понимал. Но, тем не менее, мы иногда скидывали ему прям куски, чтобы показать, как работает тот или иной процесс, и ему всё было понятно. Вот в чём сила DDD.
И, кстати, языка Ruby. Мне под прошлым видео накидали комментов, что "мёртвый язык, нужно уходить с него". Нет, такие DSL, как на Ruby, не напишешь ни на одном языке. Ruby + DDD — это очень крутой тандем. Если что, я не топлю вообще за Ruby. Любой язык — это просто инструмент, где-то он уместен, где-то нет. Но сейчас о другом. Вот для сравнения, другой проект российский, где не применяется DDD на корпоративном уровне. Самый интересный пример: есть вкладка в интерфейсе, написано "Управление заявками". Ты на него переходишь, там табличка. Данные в эту табличку подгружаются из API, которая называется "conversations reports". Conversation — это беседа с английского. То есть "управление заявками", а запрашиваю "беседы". Ещё и "отчёты по беседам". А внутри это API берёт просто данные с таблицы "conversations", то есть никакими портами там и не пахнет. И как вишенка на торте, это меню находится в разделе "Аналитика". То есть "управление заявками" находится в "Аналитике", обращается на API "отчёт по беседам", которая даёт просто "беседы". А рядом ещё одно меню "Управление звонками". Про неё продакт-менеджер периодически спрашивает: "А что мы там видим? Звонки или конверсейшн?" Если что, никого тут не обвиняю, потому что знаю, в каких условиях разрабатывали этот проект: постоянно спешка, сильно подумать никому не давали. Но, тем не менее, если бы на корпоративном уровне хоть немного пытались внедрять культуру единого языка, выделяли время на коррекции, всё было бы гораздо лучше. Но тут, опять же, спорно. Не каждому бизнесу это нужно. Бизнес может устраивать, если в некоторых его частях будет небольшой бардак. Сейчас об этом как раз и поговорим.
Переходим к следующей концепции DDD — это подобласти. Итак, традиционный подход к проектированию приложений — это составление единой модели сразу для всего бизнеса или предприятия. Я в одном из уроков упоминал одногруппника, который писал программу для завода. Там реально была одна программа для всего завода, огромная, на миллионы строк. Это колоссальная задача. Он делал эту программу много лет, и наверняка, кроме него, никто не сможет это поддерживать и развивать. Да и он, наверное, не сможет. К тому же, создание единой модели, на которой будут согласны все подразделения, — это порой даже невозможно. Во-первых, такая модель будет слишком большой, а поэтому запутанной и сложной. А во-вторых, разные отделы компаний могут использовать один и тот же термин для совершенно разных концепций или, наоборот, разные термины для одной и той же концепции. DDD позволяет избежать этих проблем за счёт выделения подобластей в бизнесе, каждый из которых будет иметь свою небольшую модель и, соответственно, свой единый язык, о котором мы поговорили выше. И соответственно, каждая такая подобласть будет решать свою бизнес-задачу.
Пример. Давайте попробуем поделить бизнес какого-нибудь маркетплейса типа Ozon или Wildberries на подобласти. Мы все примерно представляем, как они работают, и можем пофантазировать, как там всё устроено изнутри. Но это будут просто мои догадки, я никак не связан с маркетплейсами. Естественно, мы не будем описывать весь бизнес. Итак, первый поддомен, который можно выделить, — это сам маркетплейс. Знакомый нам каталог товаров, где мы ищем что-то и заказываем. Там у нас есть свой кабинет покупателя, мы можем посмотреть свои заказы, корзину и прочее.
Следующий поддомен или подобласть — это логистика. Правильно? Это тоже, по идее, сильная сторона маркетплейсов. У них куча пунктов выдачи заказов, быстрая доставка. Это всё требует больших затрат, постоянно нужно развивать. И, скорее всего, логистикой занимаются отдельные отделы. Ну и, кстати, мы тут просто гадаем. А вообще, поддомены обычно легко выделяются по отделам в компании. То есть, обычно структура подобластей повторяет структуру компании. Также очертить подобласти помогают те же самые эксперты предметной области. То есть, обычно у компании есть много видов деятельности, и для каждого — свой эксперт или эксперты, которые управляют этой деятельностью. Вот они и должны помогать очерчивать эти подобласти.
Следующей под областью я предлагаю выделить склад. Наверняка у маркетплейсов огромные склады, на которых довольно тяжело что-то найти. И наверняка у них есть софт, который помогает работникам склада строить для них оптимальный маршрут для быстрой сборки заказов, также и помогает найти место для разгрузки товара, погрузки, упаковки и прочее.
Следующая подобласть — это приём платежей. Без неё никак. Как-то нужно оплачивать товары. Также выделим службу поддержки, потому что нужно решать проблемы и споры клиентов, оформлять возвраты и вообще делать клиентов счастливыми. И последним выделим поддомен уведомлений. Не в каждой компании уведомления заслуживают лого поддомена. Здесь система довольно сложная, нужно уведомлять и продавцов, и клиентов, и доставщиков, и пункты выдачи. Поэтому из-за сложности вполне можно выделить отдельную подобласть.
Итак, мы выделили наши подобласти. Что дальше? Далее DDD предлагает нам определиться, к какому из трёх типов относятся наши поддомены. Всего есть три типа.
Первый тип — это основная подобласть или Core Subdomain. Это ключевая часть системы, которая даёт именно конкурентное преимущество для вашего бизнеса. Это то, что делает компанию уникальной. Поскольку никакая компания не может быть лучше во всех областях, то нужно определить, что будет являться смысловым ядром, и выбрать какую-то подобласть или даже подобласти как основные, как ядро, и кинуть на них основные силы. То есть, именно эта часть системы должна иметь самую лучшую и гибкую архитектуру, потому что именно эта часть будет постоянно меняться с развитием бизнеса. Именно здесь нужно искать и внедрять какие-то инновации. Соответственно, именно над этим под доменом должны работать лучшие программисты. В нашем примере я бы назвал ядром две подобласти: это маркетплейс и логистику, потому что, как мне кажется, без логистики, пунктов выдачи заказов, быстрой доставки и так далее, Ozon, Wildberries никому бы не были нужны. Это их конкурентное преимущество. Ну и без маркетплейса, соответственно, тоже. Возможно, и склад нужно отнести к ядру. Я не знаю, насколько там всё инновационно. Это, конечно, должен решать сам бизнес.
Подобласти — это вспомогательные подобласти или Supporting Subdomain. Такие поддомены, их может быть много, не являются ключевыми для бизнеса, но поддерживают работу основного поддомена. Без них основной поддомен не сможет нормально функционировать. Но функциональность таких поддоменов не уникальна. В нашем примере я бы отнёс все остальные поддомены к вспомогательным. Такие поддомены обычно не требуют больших усилий. Здесь можно пренебречь крутой архитектурой, на них не нужно сажать самых крутых специалистов. Но они всё равно важны для бизнеса. Внимание к ним тоже должно быть достаточно.
И третий, последний тип подобласти — это Generic Subdomain, общие или универсальные подобласти (по-разному их переводят). Это часть системы, которая не уникальна для бизнеса, и в целом для неё можно использовать какое-то стороннее, уже готовое решение. Это, допустим, такие задачи, как обработка платежей, аутентификация, телефония, возможно, даже какие-то системы поддержки и прочее. Сюда обычно не рекомендуют вкладывать слишком много средств и усилий. В нашем примере я не придумал таких подобластей. Обычно сюда можно попытаться отнести поддержку, потому что, ну, часто встраивают какое-то стороннее готовое решение, и всё. Но в нашем случае для таких сервисов, как Ozon, поддержка важна. Если вы будете быстро и качественно решать проблемы клиентов, то они будут оставаться с вами всю жизнь, да? А если поддержка будет хромать, то клиенты будут уходить к конкурентам. Есть сервисы, которыми люди пользуются редко, там в целом можно пренебрегать поддержкой и не вкладывать больших усилий, они никогда не окупятся.
Итак, мы определили, к каким типам относятся наши подобласти. Это разграничение необходимо, потому что оно заставляет задуматься, а какая часть программы для нас самое важное и куда нужно вкладывать как можно больше сил. Это важно даже на тактическом уровне, потому что помогает выстроить приоритеты, а также понять, где нужно заморачиваться над архитектурой кода, а где нет. Потому что, опять же, какой-то крутой, продуманный код — он не везде нужен. Он нужен только в тех частях, где он будет активно развиваться и часто меняться. Какие-то части программы нужно максимально оптимизировать, чтобы они быстро работали, пусть даже ценой непонятного кода. Какие-то части, наоборот, нужно максимально понятно написать, даже пожертвовав производительностью. А какие-то нужно просто быстро написать, и неважно, как оно будет работать и как будет написано. Всё это вы будете понимать на этапе проектирования и сразу сможете закладывать нужные сроки и сложности.
Также нужно отметить, что в каждом поддомене будет свой единый язык. Потому что, во-первых, так вы уменьшаете сложность бизнес-модели. А во-вторых, в разных поддоменах одно и то же понятие может иметь разные значения. Допустим, в каталоге продуктов понятие "продукт" — это товар, который предлагается покупателям на сайте. Здесь основное внимание уделяется описанию, фотографиям, видео, цене, скидкам и прочему. Поддомен "склад": "продукт" — это уже физическая единица, с которой работают с точки зрения размещения, упаковки, веса, габаритов, количества на складе и так далее. В поддомене "финансов" (кстати, мы его тут не отобразили) "продукт" связан с затратами на покупку, транспортировку, рекламу, налоги, продажи и так далее. И всё это будет влиять на реализацию. В каждом поддомене будут разные поля у сущности "продукт", у него будут разные связи с другими сущностями, и его будут окружать разные [музыка] бизнес-иммиграции.
Для этого в DDD существует концепция ограниченных контекстов, или Bounded Context. Ограниченные контексты чем-то похожи на подобласти, и в каких-то редких случаях даже совпадают с этими подобластями. Но на самом деле, ограниченные контексты — это технические границы вашего приложения, а подобласти — это бизнес-границы. С технической точки зрения, одна подобласть может быть реализована несколькими ограниченными контекстами. Допустим, маркетплейс включает разные бизнес-процессы: это каталог товаров, оформление заказа, кабинет покупателя и другие. С точки зрения бизнеса, это может быть одна подобласть, но с технической точки зрения, это может быть несколько ограниченных контекстов. Может быть и наоборот: несколько под областей могут быть реализованы в одном ограниченном контексте. Это тоже не запрещено. А может и полностью совпасть ограниченный контекст с подобластью. Тоже случается. В границах контекста в основном действуют те же правила, что и внутри подобласти: термины должны иметь однозначный смысл, двусмысленности быть не должно. Если нашёлся термин или модель, которая имеет разный смысл в зависимости от ситуации, то это как раз сигнал для создания отдельных контекстов с разными моделями. Ну, как пример, опять же, модель "клиент" для отдела продаж и отдела поддержки имеет абсолютно разные значения. Для отдела продаж важны контактные данные, стадия воронки продаж и тому подобное. А для отдела поддержки важны история запросов в службу поддержки, статус заявок, статус подписки и так далее. Соответственно, если мы, допустим, возьмём операцию "создание клиента", то она должна быть абсолютно разной для этих контекстов. Если попытаться их совместить, то для начала модель получится огромной, поскольку в ней нужно совместить все связи для разных отделов, поля, валидации. И соответственно, эти поля и валидации тоже будут запутанными. Поэтому в таких случаях важно разделять контекст и в каждом из них создавать свою версию модели "клиент" и связанных с ней операций.
Давайте попробуем выделить ограниченные контексты для нашего примера. Итак, в маркетплейсе у нас будет кабинет покупателя, кабинет продавца, каталог товаров, заказы. Скорее всего, будет ещё куча всяких разных контекстов, но нам для примера хватит. Далее, в логистике у нас, допустим, может быть управление пунктами выдачи заказов, курьерская доставка, доставка до ПВЗ, управление курьерами и другие. В платежах — это обработка кредитных карт и интеграция с собственным банком. Склад — это у нас могут быть управление запасами, какое-нибудь прогнозирование спроса и оптимизации, управление размещением товаров, отгрузка, упаковка, маршрутизация и оптимизация сборки. В службе поддержки может быть управление обращениями, какая-нибудь база знаний, телефония, чаты в реальном времени. Ну, соответственно, аналитика и отчёты. И в уведомлениях у нас могут быть контексты SMS, email и пуш-уведомлений. Ещё раз повторюсь, я всё это наобум придумываю, я ни с каким маркетплейсом не связан, поэтому это просто мои догадки.
Зачем нужны эти контексты и почему они, собственно, ограниченные? Дело в том, что эти области должны быть чётко очерчены, и все должны знать, где проходит граница каждого контекста. Это нужно, во-первых, для разграничения зоны ответственности. Во-вторых, для так скажем, лучшего распараллеливания работы. То есть, вы можете формировать маленькие команды, и каждый давать в ответственность один маленький ограниченный контекст. То есть, подобластью, грубо говоря, занимается целый отдел компании, целый этаж, а контекстом занимается небольшой кабинет в этом отделе. Вот случай из практики: у нас работало три команды над одним монолитом, можно сказать, над одним контекстом. И у нас вылез какой-то баг, и прям хорошо так насыпал в центре, десятки тысяч ошибок в неделю. Я иду к своей команде, говорю: "У нас тут баг". Мне говорят: "Нет, это не наш". Понесли его в другую команду. Приносим в другую команду, они говорят: "А это и не наш, это общий". Ну и типа, вы нашли, вы исправляйте. Ну окей, мы задачку себе взяли. Но взяли её недели так через четыре, потому что, ну, код же общий, мало ли, может, другая команда раньше исправит. Да и спроса ни с кого не будет, потому что код же общий, общий — значит ничей. Значит, можно и подождать с исправлением. А когда это чётко твоя зона ответственности, когда все это знают, хочешь не хочешь, но ты должен взять этот баг в спринт и быстро исправить.
Как же выглядит ограниченный контекст на практике? Если мы говорим о вебе, то, конечно же, на ум сразу приходит микросервисная архитектура. Каждый ограниченный контекст — это отдельный микросервис или даже несколько микросервисов. Это идеальный случай. Тогда у каждой команды своя кодовая база, своя модель, свой единый язык, свои деплои, то есть максимальная автономность. Если же у вас монолит, то подход DDD всё равно будет полезен. Тактические шаблоны мы ещё поразим, но стратегически DDD говорит нам, что у нас должны быть обособленные ограниченные контексты. И контексты могут общаться между собой с помощью событий. То есть, даже в монолите вы можете создавать отдельные модули, которые будут принадлежать разным командам и общаться между собой с помощью событий. События хороши тем, что код из одного ограниченного контекста не будет вызывать код из другого ограниченного контекста. Таким образом, они будут независимы друг от друга, но в то же время подписываться на события друг друга. Пока это будет монолит, события могут быть организованы прямо обрабатываться прямо в том же процессе. А могут, в принципе, асинхронно. А потом, если вы захотите выделить контекст в микросервис, то просто напишите новый адаптер для событий и будете пускать их в Kafka или ещё куда-то. То есть, в целом, это может быть планом по распиливанию монолита на микросервисы. О тактических шаблонах, как это реализовать на практике, можем поговорить в отдельном видео, если вам интересно. У нас был неплохой опыт выделения ограниченных контекстов, мы это делали в Ruby on Rails, но в целом подход можно применить в любом языке. Может, как раз ради видео доработали наш реворк.
Итак, у нас есть ограниченные контексты. Какой следующий шаг? Поскольку ограниченные контексты — это уже шаг к техническому решению, то мы должны проставить связи между контекстами. Это ещё не точное техническое решение, поэтому здесь не нужно определяться с технологиями, то есть как именно будут общаться контексты: через REST, gRPC или Kafka. Но нужно проставить просто взаимоотношения между ними. По Эрику Эвансу, у нас есть семь видов связи между контекстами. Давайте перечислим основные.
Первый вид — это партнёрство. Контексты работают тесно друг с другом и разделяют общую ответственность за выполнение задачи. Этот тип связи требует регулярной коммуникации и координации между командами. Допустим, кабинет продавца и каталог товаров могут быть такими контекстами. Если вы добавляете возможность для продавцов добавлять скидки или видео для товаров, то вы должны их отобразить в каталоге. То есть, здесь в любом случае будет тесное сотрудничество между командами, поэтому, возможно, их нужно будет посадить в соседних кабинетах.
Второй вид связи — это потребитель-поставщик. Один контекст-поставщик предоставляет необходимые функции другому контексту-потребителю. Потребитель зависит от поставщика, но поставщик не зависит от потребителя. Может определять свои требования к поставщику через контракты. Это, допустим, контекст каталога и доставки до ПВЗ. В каталоге нам нужно для каждого товара быстро показывать сроки доставки до клиента. Соответственно, контекст доставки будет поставщиком информации, каталог товаров — потребителем. Обратите внимание, мы решили, что эти контексты входят в ядро бизнеса, но это не значит, что они обязательно должны образовывать партнёрство.
Третий вид связи — это антикоррупционный слой, или предохранительный уровень (Anti-Corruption Layer). Используется, когда нужно интегрироваться с другим контекстом или внешней системой, не позволяя загрязнять свой контекст чужими моделями. То есть, создаётся слой, который трансформирует данные из внешнего контекста в понятные, удобные для нашего. Здесь хороший пример будет с поддержкой и телефонией. Допустим, у вас есть контекст управления обращениями, там у вас есть такие понятия, как клиент, обращение, беседа, оператор и прочее. А когда вы подключаете стороннюю телефонию, там будет какое-то API для получения списка звонков, возможно, записи бесед, какие-то события вроде "звонок начался", "звонок закончился". В общем, всё это не будет вписываться в вашу модель. И чтобы не засорять вашу модель понятиями с телефонии, которая, кстати, может поменяться в любой момент, если она не устроит бизнес, вы создаёте этот антикоррупционный слой, который будет преобразовывать событие "звонок начался" в "обращение телефо", "клиент телефо", клиентов и прочее.
Четвёртый вид связи — это служба с открытым протоколом. Контекст открывает определённый набор интерфейсов, который может быть использован другими контекстами. Обычно это просто какие-то публичные API, которые позволяют взаимодействовать с этим контекстом. Тут пример — это какой-нибудь контекст уведомлений. Вы создаёте микросервис, который имеет какое-то API REST или слушает Kafka топик, и если какой-то микросервис хочет отправить сообщение клиенту, то просто посылает команду микросервису уведомлений. Отличие службы с открытым протоколом от потребителя-поставщика в том, что постсервис публикует публичные API, и любой контекст может им пользоваться. Но по сути, это независимый ни от кого контекст. А потребитель-поставщик — это связь, когда потребитель просит поставщика делать какие-то фичи для него, то есть по сути является заказчиком фичей, а поставщик делает всё по запросам потребителей. Да, потребителей может быть много.
Пятый вид связи — это конформист. Это когда один из контекстов принимает модели и принципы другого контекста, обычно потому, что у него нет достаточных ресурсов для их изменения. В нашем примере конформистом мог бы стать контекст "кабинет покупателя" по отношению к "заказам" и некоторым другим контекстам, потому что в кабинете нам нужно отображать заказы, отображать обращение в поддержку и прочее. Здесь нет смысла создавать какую-то свою новую модель, можно использовать уже проработанную вышестоящую модель.
Шестой вид связи — это общее ядро. Это отношения между двумя и более группами, которые совместно используют маленькую, но общую модель. Общее ядро часто очень трудно выделить и обслуживать, потому что для этого нужно поддерживать общение между группами и обеспечивать постоянное согласование общей модели. На самом деле, опасная связь. Конечно, бывают ситуации, когда два контекста очень сильно связаны, и изменения в одном контексте почти всегда требуют изменений в другом. Тогда использование общего ядра может быть допустимым компромиссом. Но в
других случаях это может вызвать огромные проблемы. Допустим, я работал в проекте, где сначала сделали большой Монолит, назвали его Core, а потом начали пилить микросервисы. И поначалу микросервисы запрашивали данные с Core через API. Всё было хорошо, но потом клиентов стало больше, производительность стала страдать, уже не вывозил да такую нагрузку. И решили перейти на Кафку. И сетевой этой компании написал библиотеку, которая просто на каждое изменение записи в базе отправляла в Кафку эту запись целиком, а микросервисы могли подписаться на эти изменения. И по сути, на микросервисах получалась полная копия таблиц СР. Реализация была отличная, всё стабильно работало. Но допустим, для подсчёта цены товара нужно было прочитать данные из таблиц. То есть там была довольно тяжёлая бизнес-логика, и вся эта логика перетерта. Всё это, то есть, как раз что-то вроде общего ядра получилось. Но в данном случае это жуткий антипаттерн, жуткий антишахматы данных. Это шаг к комичной модели, то есть когда у вас просто идут события вроде "заказ создан", "заказ обновился", "заказ удалился" другим микросервисам. Ну или контекстом довольно сложно понять, а что вообще произошло. Что значит "заказ изменился"? Он подтвердился или отменил, или статус доставки изменился? Что конкретно произошло? Мы, допустим, делали специальный уровень, который пытался понять, что произошло, и уже в нашем микросервисе генерирования бизнеса. То есть события должны описывать бизнес-действие, а не просто говорить, что какой-то объект изменился.
Итак, для чего нужно определять типы связи? Представьте, что ваш проект — это город, разделённый на районы. И каждый район — это отдельный ограниченный контекст. Если заранее прописать, как разные районы взаимодействуют друг с другом, вы избежите хаоса. Например, дорога, которая связывает деловой район с жилым, не решит, какая дорога нужна. Можно натолкнуться на пробки или слишком долгие и неудобные маршруты. Если же вы задали правила взаимодействия между районами, то и в будущем проектирование, поддержка города станут проще и предсказуемее. Так и с DDD. Давайте рассмотрим, какие преимущества нам дают прописанные заранее типы связи. Ну, во-первых, так можно эффективнее организовать команды. То есть будет понимание, какие команды можно сделать полностью независимыми, возможно, даже нанять, а какие наоборот, нужно посадить в соседних кабинетах. Во-вторых, когда вы видите карту контекстов, проще понять, где у системы узкие места, либо система получается слишком запутанной и нужно принять меры по упрощению, где получилась слишком сильная связанность, где слабая. И таким образом можно на этапе проектирования принять решение по поводу того, где выделить микросервисы, где Монолит. Также можно решить, какие связи пересмотреть и сделать проще. Третье, если команда связи между контекстами, они могут предвидеть, как изменения в одном контексте повлияют на другие и планировать работу более эффективно. Например, если контекст имеет связь "клиент-поставщик", то изменения в поставщике могут потребовать изменений и у клиента. Четвёртое — это оптимизация производительности и масштабируемости. Зная, какие контексты тесно взаимодействуют, какие могут быть изолированы, архитекторы могут определить оптимальную стратегию для распределения нагрузки и масштабирования. Например, контексты с партнерскими связями могут требовать совместного масштабирования, а независимые контексты можно масштабировать отдельно.
На самом деле, можно придумать ещё миллион пунктов "за", и всегда будет только один "против" — это опять же время на планирование. Всегда нужно потратить время, но если проект большой, то это время потом 100% окупится. По стратегическим шаблонам мы вкратце пробежались, но можно ещё упомянуть технику Истомин. Это тема отдельного видео. Но если коротко, то эта техника, если её применять на стратегическом уровне, помогает описывать бизнес-процессы. Как это работает? Допустим, вы проектируете тот же маркетплейс. Вы собираете команду, всех, кого можете, сажаете всех в один офис, рисуете линию — это типа такой таймлайн — и начинаете клеить стикеры. На стикерах пишете какие-то события, которые происходят в вашем бизнесе. Допустим, "продавец зарегистрировался на платформе" — событие, пишем на оранжевых стикерах "seller registered". Когда проектируют более низкоуровневые процессы, рядом ещё пишут команду на синем стикере "register seller", которая собственно и порождает это событие, и агрегат, к которому относится событие "селлер" на жёлтом стикере. Что такое агрегат? Расскажу позже. Но в нашем случае, в принципе, можно ограничиться только событиями. Да, для стратегического проектирования также можно на фиолетовых стикерах отображать акторов, то есть роли тех, кто выполнил действие. Допустим, здесь продавец сам зарегистрировался. Далее, допустим, аккаунт продавца проверили и активировали. Пишем событие "селлер аккаунт активирован", а сделал это какой-то модератор. И далее можно выстроить цепочку: продавец добавил товар, покупатель сделал заказ, заказ собрали на складе, отвезли, покупатель получил заказ. И так постепенно вы уточняете все свои бизнес-процессы и выстраиваете, пока у вас не получится что-то вроде такого. И далее вы уже можете делить всё это на подобласти и на ограниченные контексты, можно группировать события по смыслу, по агрегатам и так далее. Событийный штурм помогает команде, а иногда и самим основателям, многое узнать о бизнесе и даже, возможно, переосмыслить его. Потому что когда команда собирается вместе, каждый задаёт свои вопросы, вносит какие-то коррективы, рождается много новых идей, а также команда получает много знаний о том, над чем они собственно будут работать. Обычно эти штурмы проходят мегапродуктивно. И как мы обсуждали выше, в результате штурмов очень хорошо пополняется единый язык. Если вам интересно, можем сделать отдельное видео о событийных штурмах.
Недостатки стратегических шаблонов. О достоинствах, думаю, смысла говорить нет. Кто слушал, так понимает, что стратегические шаблоны помогают лучше понять бизнес, выделить ядро, сконцентрироваться на конкурентных преимуществах, управлять сложностью модели и так далее. Также стратегические шаблоны помогают сделать хороший дизайн системы. Как говорится, альтернатива хорошему дизайну — плохой дизайн, а не отсутствие дизайна вообще. DDD также помогает всё разложить по полочкам и в результате обсуждений открыть новые точки роста, новые возможности, оптимизировать процессы и многое-многое другое. Допустим, сейчас к DDD относят чуть ли не всё, что связано с бизнесом. Допустим, вот эти книги Остервальдера тоже туда относят, хотя ни в одной из этих книг вы не встретите и намёка на DDD. Просто сообщество развивается, ищет новые инструменты, новые подходы. Поэтому я и говорю, что тема очень большая. Также на слайде и под видео я оставлю ссылку на GitHub репозитории. Там собрано очень много информации о DDD, и там же вы встретите методики из этих книг. Это не реклама, если что, я ничего не продаю. А то мне недавно написали, что я инфоцыган. Скорее всего, это какой-то сумасшедший. Но мало ли, может, ему кто-то от моего имени что-то пытался продать. Что имеете в виду? Я ничего не продаю и никому писать не буду. И всё-таки о недостатках стратегических шаблонов. Как мне кажется, основной недостаток только один: всё это отнимает слишком много времени. Понятное дело, что вы в любом случае должны потратить какое-то время на проектирование, но в случае с DDD, если у команды нет опыта, это может сильно затягиваться. Тут главное понять, что ваша модель — она живая, она будет ещё долго уточняться, она будет развиваться. Не нужно с первого захода пытаться спроектировать идеальное решение по всем правилам DDD. Нужно потратить достаточное время на проектирование, но не переусердствовать. Хотят изучать его, и даже если изучают, то всё равно всегда есть риск неправильного выделения подообластей или ограниченных контекстов. А это ведёт к дополнительным сложностям и проблемам. Также DDD — это не какая-то палочка-выручалочка, не везде он нужен, особенно в небольших проектах, он там избыточен. Ну, стратегические шаблоны ещё можно и техники как-то применить на маленьких проектах, тот же Истомин, но не более. Особенно не стоит лезть в тактические шаблоны в небольших на небольших проектах. На этом, думаю, по основным недостаткам всё. Переходим к самой противоречивой, местами недооценённой и поэтому, наверно, самой сложной части — к тактическим шаблонам. Если стратегические шаблоны описывают высокоуровневую архитектуру и отвечают на вопрос "что нужно разработать?", то тактические шаблоны — это низкоуровневые инструменты, которые отвечают на вопрос "как лучше всего запрограммировать бизнес-логику?". То есть эти шаблоны помогают разработчикам реализовать доменную модель внутри ограниченного контекста. Обратите внимание на последнюю фразу: "внутри ограниченного контекста". То есть тактические шаблоны всё-таки лучше использовать вместе со стратегическими, потому что DDD в целом помогает управлять сложностью вашего приложения, и без деления на ограниченные контексты одни тактические шаблоны вам сильно не помогут. Но в то же время стратегические шаблоны можно использовать и без тактических, если последние вам вдруг не понравятся. В чём практический смысл тактических шаблонов? Почему они важны? Ну, во-первых, как я сказал, они упрощают моделирование сложного доменного кода. То есть они помогают разделить сложный бизнес-код. По сути, эти шаблоны дают вам некую структуру, которой вы следуете и получаете хороший код. Во-вторых, тактические шаблоны помогают изолировать доменную логику от инфраструктурных деталей. То есть, если вы решите сменить базу данных или брокер сообщений, то это никак не должно затрагивать бизнес-логику. В идеале домен даже не должен зависеть и от фреймворка, хотя бывает, этого очень тяжело добиться. В-третьих, доменная логика должна соблюдать инварианты, то есть важные бизнес-правила. Допустим, пока продавец не прошёл верификацию, его товары не должны публиковаться на маркетплейсе, или заказ может быть подтверждён только если товары есть на складе и не забронированы другими клиентами. Другими словами, домен должен обеспечивать согласованность данных. Бывает, эту задачу перекладывают на базу данных, то есть создают всякие индексы, внешние связи, пишут функции. Но DDD говорит нам, что нет, это задача домена. В-четвёртых, эти шаблоны повышают, так скажем, читабельность и поддерживаемость кода. В идеале новый человек должен прийти в команду и понять даже по структуре папок, чем занимается этот ограниченный контекст, и при необходимости легко внести правки. Ну и последнее: они улучшают тестируемость. Тестировать модели и проще, и тесты можно сделать быстрее. Допустим, если использовать шаблон "репозиторий", то вы можете сделать свою супербыструю реализацию репозитория для тестов и тем самым сильно ускорить их. Допустим, в одном из проектов у нас с тестами была огромная беда из-за того, что логика размазана. В каждом тесте приходилось создавать в базе кучу необходимых объектов. В итоге тесты шли минут по 30, а из-за этого деплой проходят по полтора-два часа. А если ещё и миграции нужно катить, а они катятся отдельно, то в итоге на деплой может уйти и целый день. Поэтому быстрые тесты тоже важны.
На самом деле, давайте вкратце рассмотрим эти шаблоны. Первый шаблон — это сущность или Entity. Entity — это объект, который существует в реальном мире. Сущности имеют свой уникальный идентификатор, и даже если две сущности имеют одинаковые атрибуты, допустим, два клиента имеют одинаковое имя, фамилию и дату рождения, они всё равно будут разными сущностями. Они будут отличаться по идентификатору. Также сущности могут изменяться во времени, то есть тот же клиент может сменить фамилию, номер телефона, почту, но он всё равно останется той же сущностью.
Следующий шаблон — это Value Object или объект значения. Это объект, который определяется своими атрибутами, а не идентификатором. Он неизменяемый, у него нет уникального идентификатора. Любые изменения просто создают новый объект. Соответственно, и сравнивают такие объекты по значению, а не по идентификатору. Примером может быть адрес доставки. Адрес не живёт своей жизнью, он не меняется со временем, поэтому это не сущность, а объект. Есть некий список характеристик, по которым можно понять, что это Value Object, а не сущность: первое — это он измеряет, оценивает или описывает объект предметной области; его можно считать неизменяемым; он моделирует нечто концептуально целостное, объединяя связанные атрибуты в одно целое, как адрес; да, при изменении способа измерения или описания его можно полностью заменить; и последнее — его можно сравнивать с другими объектами с помощью отношения равенства значений. Нужно стараться использовать объекты значения там, где это возможно, вместо сущностей, потому что их проще создавать, проще тестировать и работать с ними тоже проще.
Следующий шаблон — это агрегат. Агрегат — это группа связанных сущностей и объектов значений, которые управляются как единое целое. У агрегата всегда есть какой-то корневой элемент — это какая-то сущность, которая является корнем. Как пример, это заказ и его позиции, и Order Line Item. Также можно добавить сюда адрес доставки. Здесь Order будет корневой сущностью, и через Order можно будет добавлять новые позиции в заказ, изменять эти позиции и назначать адрес доставки. При этом Order будет гарантировать консистентность всех данных внутри себя. Есть специальные техники для нахождения агрегатов в приложении, потому что у вас наверняка возникнет вопрос: "А что должен содержать в себе агрегат? Order может и клиента сюда включить или оставить клиента как отдельный агрегат? А может, адрес доставки должен принадлежать клиенту, а не Order?" То есть вопросов может быть много, и эта тема отдельного видео. Если вам будет интересно, дайте знать в комментариях. Также ещё говорят, что агрегат — это граница согласованности. То есть граница агрегата определяется таким образом, чтобы все изменения внутри этого агрегата происходили атомарно, можно сказать, в рамках одной транзакции, чтобы опять же все инварианты проверялись внутри этой транзакции. Можно подумать, какие сущности и Value Object вам придётся затрагивать при операциях с агрегатом, и вот они будут первыми претендентами на включение в этот агрегат. Также есть такое понятие, как "анемичная модель". Одно из её проявлений — это использование геттеров и сеттеров для сущностей и агрегатов вне этих самых агрегатов. То есть, допустим, у вас есть бизнес-операция какая-нибудь "изменить количество товаров в заказе". Вы можете сделать какой-то сервис, допустим, `updateOrder`, и в нём просто найти заказ, найти позицию заказа, обновить количество в этой позиции и сохранить. Это делает вашу модель анемичной, потому что бизнес-логика перетекает в какой-то сервис с невыразительным названием `updateOrder`. Что он обновляет, в каких случаях вообще непонятно. Совсем другое дело, если вы в агрегат Order добавляете метод `updateItemQuantity`. Теперь ваша модель уже начинает что-то... уже другие программисты, глядя на этот код, поймут, какие операции можно выполнить с этим агрегатом. Также, допустим, требования бизнеса изменились, и теперь вам нужно как-то реагировать на изменение количества товаров в заказе, допустим, посылать события в микросервис склада, чтобы там пересобрать заказ. Вы просто сможете потом изменить один метод и всё, а не искать по сервисам, где у вас меняется количество товаров, и там дописывать логику. А может, и ничего не менять, потому что обычно в каждом таком методе агрегата и так сразу генерируются события предметной области. Вы можете возразить, что Иван, но это же антипаттерн "Fat Model" — жирная модель. Люди наоборот придумывали, куда переносить эту логику из модели, лишь бы не засорять их, а тут мы возвращаемся наоборот к этому. Я отвечу, что да, вы будете засорять агрегат методами, но для этого и есть ограниченные контексты. Если вы правильно поделите приложение на ограниченные контексты, то один и тот же агрегат сможет так скажем, размазаться на несколько контекстов, которые будут общаться между собой с помощью доменных событий, и в каждом из этих агрегатов будет не так и много методов. Поэтому всё-таки, опять же, тактические шаблоны нужно использовать вместе со стратегическими.
Идём дальше. Хранилище или репозиторий. Хранилище — это абстракция для доступа к данным, которая позволяет работать с агрегатами как с коллекцией объектов в памяти. Репозиторий инкапсулирует логику доступа к данным, детали работы с базой данных и предоставляет интерфейс для поиска и сохранения агрегатов. Здесь, кстати, нужно сказать, что сущность, которая сама не имеет вложенных зависимостей и не зависит от другого агрегата, является агрегатом, состоящим из одного корня. То есть сущность "пользователь" может не иметь никаких других зависимостей, но она всё равно будет являться агрегатом, состоящим из одного корня. Обычно хранилища делают так, чтобы сохранять и извлекать агрегаты целиком. Допустим, при извлечении заказа из хранилища сразу извлекаются и адрес доставки, и позиции заказа. Это обеспечивает согласованность данных.
Следующий шаблон — это доменный сервис. Это класс, который содержит бизнес-логику, которая не вписывается в сущности или объекты значения. Эта логика не принадлежит ни одной конкретной сущности, может быть связана с несколькими сущностями или агрегатами, или выполняет вычисления, проверки и координации между разными объектами. Как пример, это может быть сервис расчёта доставки. Это явно бизнес-операция, но она зависит от многих факторов, например, вес заказа, адрес доставки, адрес вклада и так далее. То есть логика расчёта не принадлежит одному агрегату.
Следующий шаблон — это фабрики или Factory. Здесь, думаю, многие знают этот шаблон. Фабрика — это объект или метод для создания сложных объектов с учётом инвариантов. Фабрики абстрагируются, гарантируют корректное состояние объекта при его создании и упрощают создание сложных объектов с зависимостями или несколькими шагами инициализации. Примером фабрики может служить создание заказа с несколькими позициями. Там могут быть спрятаны всякие проверки на наличие на складе этих позиций, возможно, заставки до адресата и прочее.
Какие минусы у тактических шаблонов? Почему я говорю, что они могут вам не понравиться? Но для начала расскажу о своём опыте. Когда мы с другом впервые попытались использовать эти шаблоны на практике, у нас были, ну, очень жаркие споры. Кстати, друга зовут Алексей Волобуев. Лёха, привет! Надеюсь, когда-нибудь всё-таки запишем с тобой интервью. Итак, в чём была проблема? Мы обсудили фичу, я её реализовал с помощью тактических шаблонов DDD, и, честно говоря, был очень доволен собой. Я в коде описал всё, что на тот момент знал о бизнесе. Это выглядело круто, на мой взгляд. Приношу Алексею код на ревью, а он говорит: "Ты что, всё фигня? Давай сначала домен должен быть максимально абстрактный". А ты тут кучу логики написал. Я говорю: "Да как абстрактный? Зачем тогда этот DDD вообще нужен?" Не хочу такой DDD. В общем, долго спорили неделю, потом переваривали, а через неделю Алексей нашёл статью, где хорошо объяснялось, о чём мы спорили. Вот картинка из этой статьи, она описывает трилемму в DDD, связанную с полнотой модели, её чистотой и производительностью. То есть я боролся за полноту домена, Алексей — за чистоту. Что есть ещё и третья характеристика — производительность, до которой мы на тот момент ещё не дошли. И трилемма нам говорит, что невозможно реализовать модель, которая будет сочетать все три характеристики. Можно выбрать только две из них. Давайте посмотрим на примеры кода, которые показывают доступные вариации. Пример простой — это валидация email на уникальность при создании пользователя. Я думаю, все согласятся, что это бизнес-операция, это инвариант в системе, не должно быть двух пользователей с одинаковым email. Поэтому, по-хорошему, эта проверка должна быть в домене. Итак, первая комбинация — это полнота домена и производительность. Как видим, здесь мы должны в нашу entity передать репозиторий. Из-за этого теряется чистота домена. Пусть даже ваш домен будет зависеть от интерфейса репозитория, всё равно передача инфраструктурного объекта в домен считается загрязнением модели. Второй вариант — это чистота домена и производительность. Здесь мы выносим проверку на дубликат из домена и проверяем где-то в контроллере или в каком-то application service, неважно, главное, мы вынесли эту проверку из домена. А значит, мы теряем полноту нашей модели. И третий вариант: мы получаем пользователя, а также всех пользователей и передаём в метод `changeEmail`. Здесь мы получаем полную и чистую модель, но соответственно, теряем в производительности, потому что загрузить всех пользователей в память — это порой даже и невозможно. Поэтому в любом случае вам придётся с командой выбрать, что для вас важнее. Возможно, даже в каждом контексте выбор будет разный: где-то упор на производительность, где-то на полноту, где-то на чистоту. Когда мы дальше начали разбираться с этой темой, Алексей решил сделать мини-исследование в нашей компании и расспросить коллег, кто как использует DDD. В итоге оказалось, что все в компании понимают и используют DDD, но все по-разному. Все читали одни и те же книги, но поняли всё по-своему. В итоге у нас в каждом микросервисе, за который отвечали разные команды, было по две-три разных реализации этого DDD. То есть каждый, кто приходил в проект, делал что-то своё, потом уходил, приходил новый человек, делал что-то по-своему, потом уходил, приходил новый, делал опять по-своему, и так накопилось по несколько реализаций в каждом сервисе. И впоследствии руководство озаботилось проблемой и даже начали искать какой-то единый подход и даже нашли и приняли как стандарт, но переделать всё это уже не представляется возможным. Также большая проблема, что DDD даёт структуру в виде шаблонов, но как всё это реализовать в виде файлов и папок — тоже большой вопрос. Если выносить в отдельную папку "домен", а "инфраструктуру" в другую, "application service" в третью, то это совсем неудобно в плане разработки. Если совмещать всё в одной папке, то тоже получается каша. Это как в той же чистой архитектуре. Вот диаграмма из книги: всё прекрасно, всё красиво, есть интерфейсы, есть слои. Но попробуйте разложить это по файлам и папкам. Уверен, через пару недель вы не сможете всё это собрать вместе, не вспомните, что куда положили. Поэтому, да, тактические шаблоны есть, они работают, но в мире Ruby on Rails, в котором я сейчас по большей части обитаю, я верю, что можно выжать больше, применив DSL. У нас есть кое-какие наработки, мы пытались описывать бизнес-логику с помощью DSL. Что-то получалось хорошо, что-то не получалось. Ошибок, конечно, понаделали больше, чем удачных решений, но в целом есть что рассказать. Если у вас будет интерес. А на этом будем заканчивать. Если тема вам понравилась, то пишите, что именно мы можем больше поговорить про тактические шаблоны, про анемичную модель, как с ней бороться, можем поговорить про то, как отличить Entity от агрегата и Value Object. Также можем обсудить и архитектуру портов и адаптеров, потому что она хорошо сочетается с DDD. Или можем поговорить про Event Sourcing. В общем, дайте обратную связь. А на этом всё. Удачи и пока.