Почему к DDD обычно подходят не с той стороны? А с какой стороны надо? Какое отношение ко всему этому имеют жирафы и утконосы?
Специально для Хабра — текстовая расшифровка доклада «Domain-driven design: рецепт для прагматика». Доклад был сделан на .NET-конференции DotNext, но может пригодиться не только дотнетчикам, а всем интересующимся DDD (мы верим, вы осилите пару примеров кода на C#). Видеозапись доклада также прилагается.
Всем привет, меня зовут Алексей Мерсон. Я расскажу, что такое Domain-Driven Design и в чём его суть, но прежде давайте разберёмся, зачем он вообще нужен.
Мартин Фаулер сказал: «Немного есть вещей менее логичных, чем бизнес-логика». Жираф однозначно из этих немногих. Расстояние между
На первый взгляд, логики действительно нет. Но это всего лишь дремучее легаси, оставшееся со времён древних рыб. У рыб, как известно, шеи нет, поэтому этот нерв проходит по оптимальной траектории. А когда после нескольких миллионов лет рефакторинга появились млекопитающие, нерв пришлось удлинить для сохранения обратной совместимости. Ну не переделывать же из-за какого-то жирафа?
Но жираф — это ладно, ведь есть утконос.
Вдумайтесь. Млекопитающее. С клювом. Живёт преимущественно в воде. Откладывает яйца. И к тому же ядовитое. Может показаться, что единственное логическое объяснение его существования — это то, что он из Австралии.
Но я думаю, что всё банальнее. Подрядчик просто подзабил на проектирование и накопипастил со StackOverflow, ну или что там было в те времена.
Я знаю, о чём вы сейчас думаете: «Алексей, ну вы же нам обещали Domain-Driven Design, а тут какое-то „В мире животных“!»
Коллеги, что такое разработка? Разработка — это когда мы берём какую-то часть реального мира, бизнес-процесса, и превращаем её в код, то есть строим программную модель. Какие проблемы нас поджидают на этом пути?
Первое — это сложность самих бизнес-процессов, то есть сложность понимания того, как работает бизнес, какие процессы там происходят, по какой логике они построены.
Вторая проблема — это реализация этих бизнес-процессов в виде кода, использование правильных паттернов, правильных подходов и так далее. Это тоже достаточно сложная тема.
Смотрите, бизнес-процессы — они как тот жираф: начинались с простейших одноклеточных, а потом на вас смотрит «это», и никто уже не понимает ни «откуда оно взялось», ни «как оно работает».
Чтобы построить успешную модель такого процесса, нужно в первую очередь ответить на вопрос «зачем?». Зачем мы хотим построить эту модель? Каких целей мы хотим достичь? Ведь если заказчик хотел чучело жирафа, а получил кишки, то он огорчится, даже если пищеварение в этой модели реализовано на загляденье. И заказчик потеряет не только деньги и время, он потеряет доверие к нам как разработчикам, а мы потеряем репутацию и клиента.
Но даже если мы разобрались в целях, это ещё не гарантирует, что мы не получим утконоса в результате. Дело в том, что цель мало понять. Цель нужно достичь. И в этом нам помогает Domain-Driven Design.
Основная цель Domain-Driven Design — это борьба со сложностью бизнес-процессов и их автоматизации и реализации в коде. «Domain» переводится как «предметная область», и именно от предметной области отталкивается разработка и проектирование в рамках данного подхода.
Domain-Driven Design включает в себя множество вещей. Это и стратегическое проектирование, и взаимодействие между людьми, и подходы к архитектуре, и тактические паттерны — это целый арсенал, который реально работает и реально помогает делать проекты. Есть только одно «но». Прежде чем начать бороться со сложностью с помощью Domain-Driven Design, нужно научиться бороться со сложностью самого Domain-Driven Design.
Когда человек начинает погружаться в эту тему, на него вываливается огромный объём информации: толстые книжки, куча статей, паттернов, примеров. Всё это сбивает с толку, и легко, что называется, не заметить за деревьями леса. Я когда-то это прочувствовал на себе, а сегодня хочу поделиться с вами опытом и помочь продраться через эти дебри, начав наконец применять Domain-Driven Design.
Сам термин Domain-Driven Design был предложен Эриком Эвансом в 2003-м в его книге с труднопроизносимым названием, которую в сообществе называют просто «Синяя книга» (Blue Book). Проблема в том, что первую половину книги Эванс рассказывает про тактические паттерны (вы все их знаете: это фабрики, сущности, репозиторий, сервисы), а до второй половины люди обычно уже не добираются. Человек смотрит: всё знакомо, пойду запилю приложение по DDD.
Справа — то, что получится, если безумно набрасывать на компилятор тактические паттерны. Слева — если использовать стратегические паттерны.
Со времени выхода «Синей книги» сформировалось довольно сильное DDD-комьюнити, многие вещи были переосмыслены. Да и сам Эванс признавался, что уже не понимает, как он мог поставить в конец такую важную вещь, как стратегическое проектирование.
И спустя 10 лет, в 2013 году, вышла «Красная книга», автор — Вон Вернон. И в этой книге изложение построено уже в правильном порядке: начинается со стратегического проектирования, с основ. А когда читателем получена необходимая база, тогда уже начинают рассказывать про тактические паттерны и детали реализации.
Обычно в докладах по DDD рекомендуют читать Эванса, в интернетах есть даже целые руководства, в каком порядке нужно прочитать главы для правильного погружения. Я рекомендую поступить проще: начать с «Красной книги», прочитать её, а уже потом переходить к «Синей».
И коль скоро стратегическое проектирование — такая важная вещь, давайте о его ключевых идеях и поговорим.
«Ключевые идеи стратегического проектирования»
В любом проекте по автоматизации бизнеса всегда есть доменные эксперты. Это люди, которые лучше всех понимают, как работают те бизнес-процессы, которые предстоит моделировать. Это могут быть ведущие разработчики, руководители, топ-менеджеры. В общем, это может быть кто угодно, лишь бы он разбирался в тех бизнес-процессах, с которыми нам нужно иметь дело.
С другой стороны, есть технические специалисты: разработчики, архитекторы, которые занимаются непосредственно автоматизацией и реализацией приложений. В изображённом примере, наверное, заказчик хотел детскую железную дорогу, а получился эдакий монстр.
Почему так происходит? Потому что взаимодействие между техническими специалистами и доменными экспертами в типичной ситуации выглядит примерно так: между ними большая-большая стена, а по верху этой стены ходит менеджер и сначала пытается услышать, что ему орут с одной стороны стены, потом в меру связок пытается перекричать это на другую сторону стены, и так по кругу.
Иногда менеджер попадается глуховатый, тогда может выстраиваться целая цепочка таких менеджеров, что, естественно, не способствует успеху проекта. А как должно быть?
Должно быть постоянное взаимодействие. Технические специалисты, доменные эксперты — все участники проекта должны постоянно поддерживать общение, синхронизироваться, обсуждать цели, способы их достижения и зачем мы вообще всё это делаем.
И тут мы подходим к первому и, наверное, самому важному ключевому моменту и стратегического проектирования, и Domain-Driven Design вообще.
Общение между участниками проекта формирует то, что в Domain-Driven Design называется «единый язык» (ubiquitous language). Единый он не в том смысле, что он один на все случаи жизни. Как раз наоборот. Единый он в том смысле, что все участники общаются на нём, всё обсуждение происходит в терминах единого языка и все артефакты максимально должны быть в терминах единого языка, то есть начиная от ТЗ и заканчивая кодом.
Бизнес-сценарии
Для дальнейшего изложения нам нужен какой-нибудь бизнес-сценарий. Давайте представим себе такую ситуацию:
Приходит к нам директор JUG.ru Group и говорит: «Ребята, растёт поток докладов, людей, в общем, замучились всё делать вручную… Давайте автоматизируем процесс подготовки конференции». Мы отвечаем: «Окей!» — и принимаемся за работу.
Первый сценарий, который мы автоматизируем: «Докладчик подаёт заявку на доклад на конкретном мероприятии и добавляет информацию о своём докладе». Что мы видим в этом сценарии? Что есть докладчик, есть событие и есть доклад, а значит, уже можно построить первую доменную модель.
Вот у нас получается доменная модель: Speaker — докладчик, Talk — доклад, Event — мероприятие. Но доменная модель не может быть безграничной, не может охватывать всё, иначе она станет размытой и потеряет фокус, поэтому доменная модель должна быть чем-то ограничена. Это следующий ключевой момент.
И доменная модель, и ubiquitous language ограничены контекстом, который в Domain-Driven Design называется bounded context. Он ограничивает доменную модель таким образом, чтобы все понятия внутри него были однозначными, и все понимали, о чём идёт речь.
Если говорят «User», то всё сразу должно быть понятно, у него должна быть понятная роль, понятное значение, это не должен быть какой-то абстрактный пользователь с точки зрения IT-индустрии.
В нашем случае эта доменная модель справедлива для контекста подготовки конференции, так что находится в контексте, который мы назовём «Event planning context». Но чтобы докладчик что-то добавлял, изменял информацию, он должен как-то авторизовываться, ему нужно выдать какие-то права. И это уже будет другой контекст, «Identity context», в котором будут какие-то свои сущности: User, Role, Profile.
И смотрите, в чём здесь штука. Когда человек авторизуется в системе и собирается вносить какую-то информацию, физически это один и тот же человек, но в разных контекстах он представлен разными сущностями, и эти сущности не связаны между собой напрямую.
Если бы мы взяли и, например, отнаследовали Speaker от User, то мы смешали бы вещи, которые нельзя смешивать, и какие-то атрибуты могли бы перемешаться логикой. И модель потеряла бы фокус на конкретном значении, которое она имеет, будучи разделённой на несколько контекстов.
Демо: Sales service
Немного отвлечёмся от сухой теории и посмотрим в код.
Конференция — это не только подготовка контента, но ещё и продажи. Давайте представим, что уже был написан сервис для продажи билетов, а к нам приходит sales-менеджер и говорит: «Ребят! Когда-то кто-то написал этот сервис, давайте разберёмся, что-то мне непонятно, как скидка для постоянных клиентов считается».
Пообщавшись с менеджером, мы выясняем, что в целом сценарий этого сервиса такой: по нажатию на Checkout считается окончательная цена билета с учётом скидки постоянного клиента, и заказ переходит в состояние «Ожидание оплаты».
Код, который мы сейчас разберём, можно отдельно посмотреть в репозитории.
Открываем Solution, смотрим структуру:
Вроде всё выглядит неплохо: есть Application и Core (видимо, про слои человек знает), Repository… Видимо, первую половину Эванса человек осилил.
Открываем OrderCheckoutService. Что мы там видим? Вот такой код:
public void Checkout(long id)
{
var ord = _ordersRepository.GetOrder(id);
var orders = _ordersRepository.GetOrders()
.Count(o => o.CustomerId == ord.CustomerId
&& o.StateId == 3
&& o.OrderDate >= DateTime.UtcNow.AddYears(-3));
ord.Price *= (100 - (orders >= 5 ? 30m : orders >= 3 ? 20m : orders >= 1 ? 10m : 0)) / 100;
ord.StateId = 1;
_ordersRepository.SaveOrder(ord);
}
Смотрим на строчку с Price: здесь меняется цена. Зовём нашего sales-менеджера и говорим: «Вот, короче, здесь считается скидка, всё понятно»:
ord.Price *= (100 - (orders >= 5 ? 30m : orders >= 3 ? 20m : orders >= 1 ? 10m : 0)) / 100;
Он заглядывает через плечо: «О! Так вот как выглядит Brainfuck! А мне вроде говорили, что ребята на C# пишут».
Очевидно, что разработчик этого кода хорошо ответил на собеседовании про алгоритмы и структуры данных. Я на школьных олимпиадах примерно в таком же стиле писал. Через какое-то время с помощью форматирования и нецензурной лексики рефакторинга мы разбираемся, что к чему, и объясняем нашему многострадальному sales-менеджеру, что логика такая: если количество заказов за последние 3 года у человека не меньше одного, то он получает 10% скидки, не меньше трёх — 20%, и не меньше пяти — 30%. Он радостный уходит — теперь понятно, как это всё работает.
Думаю, многие читали книгу «Чистый код» Боба Мартина. Там он говорит про правило бойскаутов: «Место стоянки после того, как мы его покинем, должно быть чище, чем было до того, как мы туда пришли». Поэтому давайте этот код отрефакторим так, чтобы он выглядел по-человечески и соответствовал тому, о чём мы говорили чуть ранее, про ubiquitous language и его использование в коде.
Вот отрефакторенный код.
public class DiscountCalculator
{
private readonly IOrdersRepository _ordersRepository;
public DiscountCalculator(IOrdersRepository ordersRepository)
{
_ordersRepository = ordersRepository;
}
public decimal CalculateDiscountBy(long customerId)
{
var completedOrdersCount = _ordersRepository.GetLast3YearsCompletedOrdersCountFor(customerId);
return DiscountBy(completedOrdersCount);
}
private decimal DiscountBy(int completedOrdersCount)
{
if (completedOrdersCount >= 5)
return 30;
if (completedOrdersCount >= 3)
return 20;
if (completedOrdersCount >= 1)
return 10;
return 0;
}
}
Первое, что мы делаем — это выносим расчёт скидки в отдельный DiscountCalculator, в котором появляется метод CalculateDiscountBy customerId. Читается всё по-человечески, всё понятно: что, почему и как. Внутри этого метода мы видим, что у нас есть глобально два шага расчёта скидки. Первый: мы что-то получаем из репозитория заказов, тут всё по юзкейсу, можно даже не лезть внутрь, если это не та деталь, которая вас интересует сейчас. Факт, что мы получаем количество каких-то законченных заказов, после чего вторым шагом непосредственно скидку считаем по этому количеству.
Если мы хотим посмотреть, как она считается, мы идём в DiscountBy, и тут практически человеческим английским языком написано всё то же, что до этого было нашим «типа брейнфаком», всё ясно и четко.
Единственный вопрос, который мог бы возникнуть — в каких единицах измеряется скидка. Можно было бы в названии метода добавить слово «проценты», чтобы было понятно, но из контекста и фигурирующих чисел, наверное, большинство догадается, что это проценты, и для краткости это можно опустить. Если же мы хотим посмотреть, что там за количество заказов было, то мы пойдём в код Repository и посмотрим. Сейчас мы этого делать не будем. В наш Service мы должны добавить новую зависимость DiscountCalculator. И посмотрим, что у нас в итоге получилось во второй версии метода Checkout.
public void CheckoutV2(long orderId)
{
var order = _ordersRepository.GetOrder(orderId);
var discount = _discountCalculator.CalculateDiscountBy(order.CustomerId);
order.ApplyDiscount(discount);
order.State = OrderState.AwaitingPayment;
_ordersRepository.SaveOrder(order);
}
Смотрите, метод Checkout получает orderId, дальше получает по orderId заказ, по CustomerId этого заказа считает скидку с помощью калькулятора скидки, применяет скидку к заказу, ставит статус AwaitingPayment и сохраняет заказ. У нас на слайде был сценарий на русском языке, а здесь мы практически читаем перевод этого сценария на английский и всё понятно, всё очевидно.
Понимаете, в чём прелесть? Этот код можно показывать кому угодно: не только программистам, а QA, аналитикам, заказчикам. Им всем будет понятно, что происходит, потому что всё написано человеческим языком. Я использую это в нашем проекте, у нас реально QA может посмотреть некоторые куски, сверить с Wiki и понять, что там какой-то баг. Потому что в Wiki написано так, а в коде немножко по-другому, но ему понятно, что там происходит, хотя он не разработчик. И точно так же мы можем с аналитиком обсуждать код и обсуждать его предметно. Я говорю: «Смотри, вот так это работает в коде». Последняя инстанция у нас не Wiki, а код. Всё работает так, как написано в коде. Очень важно использовать ubiquitous language при написании кода.
Это и есть третий ключевой момент.
Есть очень много путаницы в Domain-Driven Design в таких вещах, как Domain, Subdomain, Bounded context, как они соотносятся, что они означают. Вроде все чего-то ограничивают, все какие-то кругленькие. Но непонятно тогда, в чём разница, зачем они такие разные придуманы.
Domain — это глобальная штука, это глобальная предметная область, в которой данный конкретный бизнес зарабатывает деньги. Например, для DotNext — это проведение конференции, для «Пятёрочки» — это розничная торговля товарами.
У больших корпораций может быть несколько Domain’ов. Например, Amazon занимается как продажей товаров через интернет, так и предоставлением облачных сервисов, это разные предметные области.
Тем не менее, это нечто глобальное и не может быть автоматизировано напрямую, даже исследовать это сложно. Для анализа Domain неизбежно делится на Subdomains, то есть на поддомены.
Поддомены — это части бизнеса, которые, говоря нашим языком, обладают высокой связностью, то есть это какие-то обособленные логические процессы, которые взаимодействуют между собой на каком-то крупном уровне.
Например, если мы берём интернет-магазин, то это будет формирование и обработка заказов, это будет доставка, это работа с поставщиками, это маркетинг, это бухгалтерия. Вот примерно такие куски — это то, на что делится бизнес.
С точки зрения DDD, Subdomain’ы делятся на три типа. И тут я хочу сказать ещё вот что: часто в книгах и в статьях Subdomain сокращают просто до Domain, но обычно в случае, когда это в совокупности с типом Subdomain. То есть, говоря «Core domain», имеют в виду Core Subdomain, не путайтесь, пожалуйста, в этом. Мне это взрывало
Subdomain’ы делятся на три типа.
Первый и самый главный — это Core. Core — это основной Subdomain, это конкурентное преимущество компании, то, на чём эта компания зарабатывает деньги, чем она отличается от конкурентов, её ноу-хау, как ни назови. Если мы возьмём конференцию DotNext, то это контент. Вы все пришли сюда за контентом, если бы такого контента здесь не было, то вы бы не пошли или пошли на другую конференцию. Не было бы DotNext в том виде, в котором он есть.
Второй тип — это Supporting Subdomain. Это тоже важная вещь для зарабатывания денег, это тоже то, без чего нельзя, но это не является каким-то ноу-хау, реальным конкурентным преимуществом. Это то, что поддерживает Core Subdomain. С точки зрения применения Domain-Driven Design это означает, что на Supporting Subdomain тратится меньше усилий, все основные силы бросаются на Core.
Пример для того же DotNext — это маркетинг. Без маркетинга нельзя, иначе о конфренции бы никто не узнал, но без контента маркетинг не нужен.
И, наконец, Generic Subdomain. Generic — это некоторая типовая бизнес-задача, которая, как правило, может быть автоматизирована готовыми продуктами или отдана на аутсорс. Это то, что тоже нужно, но уже не обязательно требует самостоятельной реализации нами, и даже более того, обычно будет хорошей идеей использовать third-party продукт.
Например, продажа билетов. DotNext продаёт билеты через TimePad. Этот Subdomain прекрасно автоматизируется TimePad’ом, и не нужно самому писать второй TimePad.
И наконец, bounded context. Bounded context и Subdomain всегда где-то рядом, но между ними есть существенная разница. Это очень важно.
На StackExchange есть вопрос, чем отличается bounded context от Subdomain’а. Subdomain — это кусочек бизнеса, кусочек реального мира, это понятие пространства постановки задачи. Bounded context ограничивает доменную модель и ubiquitous language, то есть то, что является результатом моделирования, и соответственно, bounded context — это понятие пространства решения. В процессе выполнения проектов происходит некий маппинг Subdomain’ов на bounded context’ы.
Классический пример: бухгалтерия как Subdomain, как процесс маппится, автоматизируется, например, 1С Бухгалтерией, Эльбой или «Моим делом» — так или иначе автоматизируется каким-то продуктом. Это будет bounded context бухгалтерии, в котором будет свой ubiquitous language, своя терминология. Вот такая между ними разница.
Если мы вернёмся к DotNext, то, как я уже сказал, билеты маппятся на TimePad, а контент, который является нашим Core Subdomain’ом, маппится на кастомное приложение, которое мы разрабатываем для управления контентом.
Размер bounded context
Есть такой момент, который вызывает много вопросов. Как выбрать правильный размер для bounded context? В книгах можно найти такое определение: «Bounded context должен быть ровно таким, чтобы ubiquitous language был полным, непротиворечивым, однозначным, консистентным». Классное определение, в стиле математика из известного анекдота: очень точное, но бесполезное.
Давайте обсудим, как же нам понять всё-таки: то ли это должен быть Solution, то ли Project, то ли namespace — какую шкалу нужно приложить к bounded context?
Первое, что можно практически везде прочитать: в идеале один Subdomain должен маппиться на один bounded context, то есть автоматизироваться одним bounded context. Звучит логично, потому что и там, и там есть ограничения какого-то обособленного бизнес-процесса, в обоих случаях какие-то бизнес-термины, фигурирует единый язык. Но здесь надо понимать, что это идеальная ситуация, у вас не обязательно будет так, и не обязательно пытаться этого достичь.
Потому что, с одной стороны, Subdomain может быть достаточно крупным, и может получиться несколько приложений или сервисов, которые будут его автоматизировать, поэтому может получиться, что одному Subdomain будет соответствовать несколько bounded context.
Но бывает и обратная ситуация, как правило, это характерно для легаси. То есть когда сделали большое-большое приложение, которое автоматизирует всё на свете на этом предприятии, тогда получится наоборот. Одно приложение — это один bounded context, там модель наверняка будет какая-то неоднозначная, но Subdomain’ы от этого никуда не делись, соответственно, нескольким Subdomain’ам будет соответствовать один bounded context.
Когда стала модной микросервисная архитектура, появилась другая рекомендация (хотя они друг другу не противоречат): один bounded context на один микросервис. Опять же, звучит логично, люди так реально делают. Потому что микросервис должен брать на себя какую-то чёткую функцию, которая внутри обладает высокой связностью, а с другими сервисами общается через какое-то взаимодействие. Если вы используете микросервисную архитектуру, можете брать для себя эту рекомендацию.
Но и это не всё. Ещё раз напомню, что Domain-Driven Design про очень многое: про язык, про людей. И нельзя абстрагироваться от людей и обойтись только техническими критериями в этом вопросе. Поэтому я написал так: один контекст равен икс человек. Раньше я думал, что икс примерно равен 10, но мы немножко подискутировали с Игорем Лабутиным (twitter.com/ilabutin) и вопрос остался открытым.
Здесь важно понять вот что: единый язык остаётся единым, пока все участники на нём разговаривают, обсуждают и все понимают его однозначно. Понятно, что бесконечное количество людей не может говорить на одном языке. Наша история человечества наглядно это показывает. В любом случае появляются какие-то диалекты, какие-то свои значения, сейчас можно даже мемы добавить и так далее. Так или иначе язык размоется.
Поэтому надо понимать, что количество людей, которые используют данный единый язык и, соответственно, принимают участие в разработке, в автоматизации, ограничено. В книгах ещё говорится о каких-то политических причинах: если две команды работают под руководством разных менеджеров и будут работать на одном bounded context’е, а эти менеджеры почему-то друг с другом не дружат, начнутся конфликты и фокус потеряется. Поэтому гораздо проще и правильнее будет сделать два bounded context’а на каждую команду и не пытаться совместить то, что не совмещается.
Архитектура и управление зависимостями
С точки зрения Domain-Driven Design, абсолютно всё равно, какую архитектуру вы выберете. Domain-Driven Design не про это, Domain-Driven Design про язык и про общение.
Но есть один важный момент, с точки зрения критериев выбора архитектуры, который нас интересует с позиции Domain-Driven Design: наша цель — максимально избавить бизнес-логику от сторонних зависимостей. Потому что, как только появляются сторонние зависимости, появляется терминология, появляются слова, которые не входят в единый язык и начинают замусоривать нашу бизнес-логику.
Разберём классический пример архитектуры: всем известная трёхслойная архитектура. Доменный слой как только не называют (здесь Business layer): и business, и core, и domain — всё это одно и то же. В любом случае, это слой, в котором располагается бизнес-логика, и если она зависит от слоя данных, значит, какие-то концепции из слоя данных перетекут так или иначе в доменный слой и будут его замусоривать.
Четырёхслойная архитектура — суть примерно та же, доменный слой всё равно зависит, и раз он зависит, к нему проберутся сторонние, ненужные зависимости.
И в этом смысле есть архитектура, которая позволяет этого избежать, — это onion-архитектура («луковая»). Её отличие в том, что она состоит из концентрических слоёв, зависимости идут снаружи в центр. То есть внешний слой может зависеть от любых внутренних, внутренний слой не может зависеть от внешних.
Самый внешний слой — это пользовательский интерфейс в глобальном смысле (то есть это не обязательно человеческий UI, это может быть и REST API, и что угодно). И инфраструктура, которая зачастую в общем тоже выглядит как ввод/вывод, та же база данных, фактически, слой данных. Все эти вещи оказываются во внешнем слое. То есть то, за счёт чего приложение так или иначе получает какие-то данные, команды и так далее, выносится вовне, и доменный слой избавляется от зависимости от этих вещей.
Дальше идёт Application layer — довольно холиварная тема, но это слой, в котором располагаются сценарии, юзкейсы. Этот слой использует доменный слой для реализации своих концепций.
В центре находится доменный слой. Как видим, он уже не зависит ни от чего, он становится вещью в себе. И именно поэтому доменный слой часто называется «Core», потому что это ядро, это то, что находится в центре, то, что не имеет зависимостей от сторонних вещей.
Один из вариантов реализации такой onion-архитектуры — это гексагональная архитектура, или «порты и адаптеры». Такую картинку я привёл для устрашения, рассказывать про это я не буду. В конце поста есть ссылочка на одну из миллиона статей про эту архитектуру, можно будет почитать.
Немного про тактические паттерны: Separated Interface
Как я уже говорил, во-первых, большинство тактических паттернов всем знакомы, а во-вторых, весь смысл моего доклада в том, что не в них суть. Но паттерн Separated Interface мне нравится отдельно, и я хочу про него сказать отдельно.
Давайте вернёмся к коду нашего микросервиса и посмотрим, что же там было с репозиторием.
В доменном слое был интерфейс репозитория IOrdersRepository.cs и его реализация OrdersRepository.cs.
using System.Linq;
namespace DotNext.Sales.Core
{
public interface IOrdersRepository
{
Order GetOrder(long id);
void SaveOrder(Order order);
IQueryable<Order> GetOrders();
#region V2
int GetLast3YearsCompletedOrdersCountFor(long customerId);
#endregion
}
}
Вот мы добавили сюда некий метод для получения заказов за последние три года GetLast3YearsCompletedOrdersCountFor.
И реализовали его в каком-то виде (в данном случае через Entity Framework, но это может быть что угодно):
public int GetLast3YearsCompletedOrdersCountFor(long customerId)
{
var threeYearsAgo = DateTime.UtcNow.AddYears(-3);
return _dbContext.Orders
.Count(o => o.CustomerId == customerId
&& o.State == OrderState.Completed
&& o.OrderDate >= threeYearsAgo);
}
Смотрите, в чём проблема. Репозиторий оказался в доменном слое, его реализация в доменном слое, но код, начиная с DateTime.UtcNow.AddYears(-3), по своей сути не принадлежит доменному слою, и не является бизнес-логикой. Да, LINQ делает его более-менее очеловеченным, но если бы здесь, например, были SQL-запросы, всё было бы совсем печально.
Смысл паттерна Separated Interface в том, что интерфейс сервисов, которые мы используем в доменной логике, объявляются в доменном слое. Речь идёт о репозиториях и тому подобных сервисах, в которых детали реализации этих сервисов не являются бизнес-логикой. Бизнес-логикой является сам факт существования этих сервисов и факт их вызова и использования в доменном слое. Поэтому интерфейс репозитория остается в доменном слое, а реализация переезжает в инфраструктурный.
Я подготовил другой вариант. Интерфейс репозитория остаётся в сборке Core, а вот реализация переезжает в Infrastructure.EF.
Таким образом, те понятия, которые не были свойственны для доменного слоя, мы вынесли в инфраструктуру. В качестве побочного эффекта мы можем подменять эту инфраструктуру какой-то другой реализацией. Но основная цель не в этом, основная цель в том, чтобы, как я уже говорил, избавить доменную логику от сторонних зависимостей.
Ещё раз о языке
Давайте поговорим ещё раз, и ещё раз, и ещё раз о языке.
В самом начале мы построили доменную модель «speaker — talk — event». Я думаю, что ни у кого она не вызвала особых вопросов.
А вот сценарий, на основе которого мы строили эту доменную модель:
Смотрите, сценарий на русском, а доменная модель получилась на английском.
Для неанглоязычных разработчиков это то, с чем приходится жить постоянно.
Каждый из вас, скорее всего, постоянно делает такой процесс: переводит с русского на английский и обратно. Тем, кто работает с англоязычными заказчиками и проектами, немножко проще, потому что требования на английском, обсуждение с заказчиками на английском, как правило, все сценарии на английском, код на английском, и остается только общение внутри команды на русском, которое быстро обрастает англицизмами (клиент — кастомер, заказ — ордер). И та когнитивная нагрузка, тот оверхед, который создаётся постоянным переводом, немножко отступает.
Но для тех, кто работает с русскоязычными доменами, особенно которые очень сложно переводятся на английский язык, этот перевод становится проблемой. Я с этим сталкивался, и это действительно проблема.
С этой точки зрения становится понятнее логика компании 1С, когда они сделали свой язык программирования на русском. Потому что бухгалтерия, финансы — это сложная предметная область, в которой очень много терминов, и усложнять её дополнительно постоянным переводом было бы очень жёстко.
Поэтому вот так выглядит код 1С. PascalCase добавляет веселья, но в целом это то, что может прочитать любой человек, который понимает всю эту сатанинскую непростую терминологию, и с ним это можно обсуждать, он поймёт этот код, подскажет, если что-то где-то взято неправильно и так далее.
Они выкрутились, но мы-то чем хуже?
Вот я перевёл, это тот же самый use case, который мы обсуждали, только теперь он на русском. Это реальный код, который реально компилируется. Смотрится забавно в комбинации с ключевыми словами C#, которые перевести, к счастью, нельзя. Вы можете реально показать этот код тому, кто не знает английский, и он поймёт, что происходит.
У нас летом в Петербурге был круглый стол, посвященный архитектуре, там зашла речь и про Domain-Driven Design. Мы обсуждали в том числе вопрос, связанный с языком, и там был человек, сказавший, что они в компании реально пишут на C# доменный слой на русском. Им это позволяет снизить порог входа в проект для новых разработчиков и в принципе снизить оверхед в понимании доменной логики.
Меня очень интересовало, сталкиваются ли они с проблемами где-нибудь на Continuous Integration. Потому что, хотя всё везде уже прекрасно с локализацией, Юникод и всё такое, где-нибудь обязательно вылезет какая-нибудь проблема. Но они сказали, что только один раз что-то такое встречали, сейчас не помню, где именно. Скажем так, в 95% случаев у них всё работало прекрасно, хотя у них не ручная сборка, а Continuous Integration, всё настроено, TeamCity есть. Это всё работает.
Я не призываю с сегодняшнего дня выйти с доклада и писать сразу же на русском, причём даже старые проекты отрефакторить и сделать всё на русском. Нет. Но надо понимать, что есть определённое предубеждение против русского языка в среде не 1С-программистов, и понимать, что это именно предубеждение. И если в вашем случае польза для «пациента» превышает вред, то технически так можно делать. И даже есть люди, которые так делают, и у них получается.
Давайте подытожим и получим тот самый рецепт для прагматика, который изучает Domain-Driven Design.
Первое — это общение, общение и общение. Domain-Driven Design — это штука про язык, а не про технологию. Технология — это решение тех проблем, которые возникают, когда вы хотите свой код сделать человеческим в терминах ubiquitous language. Вы сталкиваетесь с техническими проблемами, и они решаются техническими паттернами, но не наоборот. То есть репозиторий не ради репозитория, репозиторий ради того, чтобы вынести реализацию из доменного слоя, а в доменном слое всё было бы читабельно.
Дальше. Всё имеет свои пределы, свои рамки. Все модели ограничены какими-то контекстами, как только вы пытаетесь объять необъятное, у вас будет фейл, потому что потеряется однозначность терминов, все запутаются, начнут называть одни вещи так, другие эдак, или наоборот, название одно, а один понимает под этим то, другой — это. И это приведёт к проблемам.
У доменной модели должно быть минимум зависимостей. Все лишние зависимости нужно выжигать огнём. Но это как стопроцентное покрытие тестами. Здесь не надо проявлять излишний фанатизм. Это не значит, что нужно добиться того, чтобы ни одной зависимости не было. Потому что тогда вам придётся писать свой DSL-язык. У вас всё равно есть зависимость от .NET-фреймворка, у вас есть ещё какие-то вещи, поэтому достичь стопроцентной чистоты, скорее всего, не получится, но это нормально. Вам нужно стремиться, чтобы этих зависимостей было минимум.
Наконец, бизнес-логика должна писаться выразительным языком. Она пишется для человека, а не для компилятора, поэтому вам нужно использовать ubiquitous language для бизнес-логики. И вы придёте к успеху.
Полезные ссылки напоследок
- Хабраблог Максима marshinov Аршинова. У него много статей, он очень хорошо пишет про DDD: например, «Как мы попробовали DDD, CQRS и Event Sourcing и какие выводы сделали». Я рекомендую его почитать, это очень будет полезно, у него очень много реальных историй.
- Блог Джимми Богарда, автора AutoMapper и фреймворка MediatR. И блог Александра Бындю, он пишет на русском и про Domain-Driven Design, как это работает в русскоязычных проектах.
- Дальше есть такой интересный сайт «F# for fun and profit», там про то, как использовать F# в Domain-Driven Design. Написано интересно, хотя местами, на мой взгляд, там шарп гнобят незаслуженно.
- И наконец, обещанная статья про порты и адаптеры, про гексагональную архитектуру.
На этом всё, спасибо, и вот ещё раз ссылочка на GitHub.
От организаторов DotNext:
Как заметил Алексей, «если бы на конференции не было такого контента, вы бы сюда не пришли». Сейчас на сайте следующего DotNext (состоится 15-16 мая в Петербурге) уже появились описания первых докладов. Полная программа будет позже, но с 1 марта билеты подорожают, так что с решением пойти выгоднее определиться уже сейчас.
Автор: Алексей Мерсон