Хабрахабр

Domain-driven design: рецепт для прагматика


Почему к DDD обычно подходят не с той стороны? А с какой стороны надо? Какое отношение ко всему этому имеют жирафы и утконосы?

Доклад был сделан на . Специально для Хабра — текстовая расшифровка доклада «Domain-driven design: рецепт для прагматика». Видеозапись доклада также прилагается.
NET-конференции DotNext, но может пригодиться не только дотнетчикам, а всем интересующимся DDD (мы верим, вы осилите пару примеров кода на C#).

Я расскажу, что такое Domain-Driven Design и в чём его суть, но прежде давайте разберёмся, зачем он вообще нужен. Всем привет, меня зовут Алексей Мерсон.

Жираф однозначно из этих немногих. Мартин Фаулер сказал: «Немного есть вещей менее логичных, чем бизнес-логика». Однако нерв, который их соединяет, достигает 4 метров. Расстояние между мозгом и гортанью жирафа всего несколько сантиметров. Сначала он идёт вниз через всю шею, там огибает артерию и потом практически тем же путём возвращается обратно.

Но это всего лишь дремучее легаси, оставшееся со времён древних рыб. На первый взгляд, логики действительно нет. А когда после нескольких миллионов лет рефакторинга появились млекопитающие, нерв пришлось удлинить для сохранения обратной совместимости. У рыб, как известно, шеи нет, поэтому этот нерв проходит по оптимальной траектории. Ну не переделывать же из-за какого-то жирафа?

Но жираф — это ладно, ведь есть утконос.

Млекопитающее.
Вдумайтесь. Живёт преимущественно в воде. С клювом. И к тому же ядовитое. Откладывает яйца. Может показаться, что единственное логическое объяснение его существования — это то, что он из Австралии.

Подрядчик просто подзабил на проектирование и накопипастил со 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);
}

Зовём нашего sales-менеджера и говорим: «Вот, короче, здесь считается скидка, всё понятно»: Смотрим на строчку с Price: здесь меняется цена.

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 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 и посмотрим. И посмотрим, что у нас в итоге получилось во второй версии метода Checkout. В наш Service мы должны добавить новую зависимость DiscountCalculator.

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, как они соотносятся, что они означают. Но непонятно тогда, в чём разница, зачем они такие разные придуманы.

Например, для DotNext — это проведение конференции, для «Пятёрочки» — это розничная торговля товарами. Domain — это глобальная штука, это глобальная предметная область, в которой данный конкретный бизнес зарабатывает деньги.

Например, Amazon занимается как продажей товаров через интернет, так и предоставлением облачных сервисов, это разные предметные области. У больших корпораций может быть несколько Domain’ов.

Для анализа Domain неизбежно делится на Subdomains, то есть на поддомены. Тем не менее, это нечто глобальное и не может быть автоматизировано напрямую, даже исследовать это сложно.

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

Вот примерно такие куски — это то, на что делится бизнес. Например, если мы берём интернет-магазин, то это будет формирование и обработка заказов, это будет доставка, это работа с поставщиками, это маркетинг, это бухгалтерия.

И тут я хочу сказать ещё вот что: часто в книгах и в статьях Subdomain сокращают просто до Domain, но обычно в случае, когда это в совокупности с типом Subdomain. С точки зрения DDD, Subdomain’ы делятся на три типа. Мне это взрывало мозг поначалу. То есть, говоря «Core domain», имеют в виду Core Subdomain, не путайтесь, пожалуйста, в этом.

Subdomain’ы делятся на три типа.

Core — это основной Subdomain, это конкурентное преимущество компании, то, на чём эта компания зарабатывает деньги, чем она отличается от конкурентов, её ноу-хау, как ни назови. Первый и самый главный — это Core. Вы все пришли сюда за контентом, если бы такого контента здесь не было, то вы бы не пошли или пошли на другую конференцию. Если мы возьмём конференцию DotNext, то это контент. Не было бы DotNext в том виде, в котором он есть.

Это тоже важная вещь для зарабатывания денег, это тоже то, без чего нельзя, но это не является каким-то ноу-хау, реальным конкурентным преимуществом. Второй тип — это Supporting Subdomain. С точки зрения применения Domain-Driven Design это означает, что на Supporting Subdomain тратится меньше усилий, все основные силы бросаются на Core. Это то, что поддерживает Core Subdomain.

Без маркетинга нельзя, иначе о конфренции бы никто не узнал, но без контента маркетинг не нужен. Пример для того же DotNext — это маркетинг.

Generic — это некоторая типовая бизнес-задача, которая, как правило, может быть автоматизирована готовыми продуктами или отдана на аутсорс. И, наконец, Generic Subdomain. Это то, что тоже нужно, но уже не обязательно требует самостоятельной реализации нами, и даже более того, обычно будет хорошей идеей использовать third-party продукт.

DotNext продаёт билеты через TimePad. Например, продажа билетов. Этот Subdomain прекрасно автоматизируется TimePad’ом, и не нужно самому писать второй TimePad.

Bounded context и Subdomain всегда где-то рядом, но между ними есть существенная разница. И наконец, bounded context. Это очень важно.

Subdomain — это кусочек бизнеса, кусочек реального мира, это понятие пространства постановки задачи. На StackExchange есть вопрос, чем отличается bounded context от Subdomain’а. В процессе выполнения проектов происходит некий маппинг Subdomain’ов на bounded context’ы. Bounded context ограничивает доменную модель и ubiquitous language, то есть то, что является результатом моделирования, и соответственно, bounded context — это понятие пространства решения.

Это будет bounded context бухгалтерии, в котором будет свой ubiquitous language, своя терминология. Классический пример: бухгалтерия как Subdomain, как процесс маппится, автоматизируется, например, 1С Бухгалтерией, Эльбой или «Моим делом» — так или иначе автоматизируется каким-то продуктом. Вот такая между ними разница.

Если мы вернёмся к 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. Смотрите, в чём проблема. AddYears(-3), по своей сути не принадлежит доменному слою, и не является бизнес-логикой. UtcNow. Да, LINQ делает его более-менее очеловеченным, но если бы здесь, например, были SQL-запросы, всё было бы совсем печально.

Речь идёт о репозиториях и тому подобных сервисах, в которых детали реализации этих сервисов не являются бизнес-логикой. Смысл паттерна Separated Interface в том, что интерфейс сервисов, которые мы используем в доменной логике, объявляются в доменном слое. Поэтому интерфейс репозитория остается в доменном слое, а реализация переезжает в инфраструктурный. Бизнес-логикой является сам факт существования этих сервисов и факт их вызова и использования в доменном слое.

Интерфейс репозитория остаётся в сборке Core, а вот реализация переезжает в Infrastructure. Я подготовил другой вариант. EF.

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

Ещё раз о языке

Давайте поговорим ещё раз, и ещё раз, и ещё раз о языке.

Я думаю, что ни у кого она не вызвала особых вопросов. В самом начале мы построили доменную модель «speaker — talk — event».

А вот сценарий, на основе которого мы строили эту доменную модель:

Смотрите, сценарий на русском, а доменная модель получилась на английском.

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

Тем, кто работает с англоязычными заказчиками и проектами, немножко проще, потому что требования на английском, обсуждение с заказчиками на английском, как правило, все сценарии на английском, код на английском, и остается только общение внутри команды на русском, которое быстро обрастает англицизмами (клиент — кастомер, заказ — ордер). Каждый из вас, скорее всего, постоянно делает такой процесс: переводит с русского на английский и обратно. И та когнитивная нагрузка, тот оверхед, который создаётся постоянным переводом, немножко отступает.

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

Потому что бухгалтерия, финансы — это сложная предметная область, в которой очень много терминов, и усложнять её дополнительно постоянным переводом было бы очень жёстко. С этой точки зрения становится понятнее логика компании 1С, когда они сделали свой язык программирования на русском.

PascalCase добавляет веселья, но в целом это то, что может прочитать любой человек, который понимает всю эту сатанинскую непростую терминологию, и с ним это можно обсуждать, он поймёт этот код, подскажет, если что-то где-то взято неправильно и так далее. Поэтому вот так выглядит код 1С.

Они выкрутились, но мы-то чем хуже?

Это реальный код, который реально компилируется. Вот я перевёл, это тот же самый use case, который мы обсуждали, только теперь он на русском. Вы можете реально показать этот код тому, кто не знает английский, и он поймёт, что происходит. Смотрится забавно в комбинации с ключевыми словами C#, которые перевести, к счастью, нельзя.

Мы обсуждали в том числе вопрос, связанный с языком, и там был человек, сказавший, что они в компании реально пишут на C# доменный слой на русском. У нас летом в Петербурге был круглый стол, посвященный архитектуре, там зашла речь и про Domain-Driven Design. Им это позволяет снизить порог входа в проект для новых разработчиков и в принципе снизить оверхед в понимании доменной логики.

Потому что, хотя всё везде уже прекрасно с локализацией, Юникод и всё такое, где-нибудь обязательно вылезет какая-нибудь проблема. Меня очень интересовало, сталкиваются ли они с проблемами где-нибудь на 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 (состоится 15-16 мая в Петербурге) уже появились описания первых докладов. От организаторов DotNext:
Как заметил Алексей, «если бы на конференции не было такого контента, вы бы сюда не пришли». Полная программа будет позже, но с 1 марта билеты подорожают, так что с решением пойти выгоднее определиться уже сейчас.

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»