Что такое доменная модель
Введение в Rich Domain Model
В последнее время можно услышать много аббревиатур, которые оканчиваются на DD: TDD, BDD, FDD, etc. Меня заинтересовал один из представителей «DD-семейства» — DDD, Domain Driven Development. Я не стану описывать здесь все тонкости этой методологии, ведь всю необходимую информацию можно легко найти в сети. Моя цель — рассказать о наиболее важной концепции DDD, о Rich Domain Model и на небольшом примере показать основные нюансы реализации.
Rich Domain Model противопоставляют Anemic Domain Model. «Толстая» модель характеризуется состоянием и поведение, в отличии от «худой», где есть только состояние. По теме могу порекомендовать презентацию. В конечном итоге для себя решил так: ничего плохого в anemic нету, но это процедурный подход со всеми вытекающими последствиями. Более глубоко вдаваться в дебри я пока не хочу, просто примем, что Rich Domain Model — это модель с состоянием и поведением (бизнес-логикой).
При более близком рассмотрении я столкнулся со сложностями понимания паттерна Repository и его отличием от DAO, но об этом позже.
Азы использования Rich Domain Model рассмотрим на примере мини-сервис блога (мне кажется блог последнее время претендует на роль Hello, World! применительно к вебу).
Начнем, как и полагается, с предметной области:
public class Post <
private String text;
private Date date;
private List comments;
public class Comment <
private String text;
private Date date;
// getters and setters.
>
Пользователи могут оставлять комментарии к постам, поэтому добавим метод comment к модели поста. На этом методе ограничимся всей бизнес-логикой нашего прототипа:
Наше приложение должно обеспечить сохранение объектов, так что очередь за Repository. Поведение Repository похоже на поведение обычной java-коллекции. Беглый взгляд на этот паттерн создает впечатление, что никакого отличия от DAO нету, но это не так. Рассмотрим пример сеанса работы с DAO:
И все! Никакого update нету. Как видите, работа с repository подобна работе с коллекцией объектов, в то время как работая с DAO мы все время должны вручную фиксировать состояние объекта в хранилище.
Вот собственно интерфейс:
public interface PostRepository <
void add(Post post);
void remove(PostId postId);
Post get (PostId postId);
List
Для доступа к элементом нужен идентификатор, поэтому я создал вспомогательный класс PostId:
public class Post <
private PostId postId;
private String text;
private Date date;
private List comments;
.
>
Я нарочно не использую численные идентификаторы, которые обычно используют в качестве первичных ключей в БД. Мы идем от домена и о БД пока ничего знать не должны. В последствии может быть реализован любой repository и не обязательно на основе БД, где к примеру может не быть числовых идентификаторов.
А вот простая реализация PostRepository:
public class InMemoryPostRepository implements PostRepository <
private Map
identityMap = new HashMap
@Override
public List
getAll() <
return new ArrayList
Теперь создадим сервис-слой. Хочу обратить внимание, что в сервис-слое не должно быть бизнес-логики, он нужен лишь что бы обозначить границы приложения, делегируя пользовательские вызовы объектам домена, которые извлекаются из repository:
Вот собственно и все. Конечно на этом примере сложно увидеть все преимущества Rich Domain Model, потому как пример слишком уж тривиальный. Но я надеюсь, что он поможет кому-нибудь в практической реализации. Когда я пытался разобраться самостоятельно, то запрос в гугле «rich domain model example» не давал ничего вразумительного. Но теперь, когда в голове уже есть более или менее целостная картина, решил поделиться своими выводами с сообществом. Если статья понравится, то я могу написать продолжение, в котором будет уже более реальная реализация на основе Hibernate и постараюсь показать на практике такое важное свойство, как Persistence Ignorance.
Отдельно хотелось бы сказать о 2-х открытиях, которые я сделал для себя ища материалы по теме в сети:
1) Если используется Hibernate или любой другой ORM, то использование DAO неуместно.
2) Учитывая большую популярность Anemic Domain Model, можно сделать смелое заявление, что ООП используется довольно редко.
Domain Model – Domain Logic Patterns (PoEAA)
Модель предметной области (Domain Model) – объектно-ориентированный шаблон проектирования. Цель проектирования предметной области – определение бизнес-объектов, которые представляют реальные сущности предметной области. При использовании модели предметной области бизнес-сущности и сущности предметной области включают и поведение, и структуру. Иначе говоря, бизнес правила и отношения инкапсулированы в модели предметной области. Проектирование предметной области требует глубокого анализа предметной области и, как правило, не сопоставляется с реляционными моделями, используемыми большинством баз данных. Применяйте модель предметной области, если предметная область содержит сложные бизнес-правила, если создается насыщенный клиент, и модель предметной области может инициализироваться и удерживаться в памяти. Модель предметной области не может применяться при работе с бизнес-слоем, не сохраняющим состояние, который требует инициализации модели предметной области при каждом запросе.
Реализация модели предметной области означает пополнение приложения целым слоем объектов, описывающих различные стороны определенной области бизнеса. Одни объекты призваны имитировать элементы данных, которыми оперируют в этой области, а другие должны формализовать те или иные бизнес-правила. Функции тесно сочетаются сданными, которыми они манипулируют. Объектно-ориентированная модель предметной области часто напоминает схему соответствующей базы данных, хотя между ними все еще остается множество различий. В модели предметной области смешиваются данные и функции, допускаются многозначные атрибуты, создаются сложные сети ассоциаций и используются связи наследования. В сфере корпоративных программных приложений можно выделить две разновидности моделей предметной области. «Простая» во многом походит на схему базы данных. Представление бизнес-логики и содержит, как правило, по одному объекту домена в расчете на каждую таблицу. «Сложная» модель может отличаться от структуры базы данных и содержать иерархии наследования, стратегии и иные типовые решения, а также сложные сети мелких взаимосвязанных объектов.
Учимся проектировать на основе предметной области (DDD: Domain Driven Design)
1. Введение
В данной статье я хотел бы рассказать об этих трёх буквах, постоянно находящихся на слуху, но для многих являющихся тайной за семью печатями, а так же привести ряд ресурсов, с которыми неплохо было бы познакомиться при желании продолжить развитие в проектировании на основе предметной области (DDD: Domain Driven Design).
2. Так почему же DDD?
Есть несколько шаблонов реализации предметной области (Domain Logic) или бизнес-логики (Business Logic):
1) Table Module – представляет собой объект, в единственном экземпляре, обрабатывающий бизнес логику для всех записей в таблице базы данных, либо представления.
2) Transaction Script – организует взаимодействие с бизнес-логикой посредствам процедур, принимающих запросы с уровня представления.
3) Domain Model – непосредственно, объектная модель предметной области, включающая в себя как поведение, так и данные.
Эти шаблоны описаны более подробно Мартином Фаулером, в его книге “Архитектура корпоративных программных приложений. Шаблоны корпоративных приложений” (Patterns of Enterprise Application Architecture (P of EAA)). В данной книге он показывает, что первые два шаблона более привлекательны в начале работы с предметной областью, однако так же обращает внимание, что при наращивании сложности логики предметной области стоит больше внимания уделять сопровождению инфраструктуры, используя первые два подхода, это время можно уменьшить, если обратиться в своём решении к третьему из вышеперечисленных шаблонов, так называемой “Модели предметной области”.
На основе этого сделаем небольшой вывод о том, что данный шаблон (“Модель предметной области”) лучше всего подойдёт, к примеру, для такой непростой области, как финансовый рынок. Большинство, создаваемого в наши дни программного обеспечения предназначено для различных нужд бизнеса, следовательно какие-то абстрактные, обобщенные решения находят своё место на рынке (с довольно таки высокой конкуренцией) всё реже и реже. К чему я пишу про всё это? Потому что DDD – это не только качественное проектирование, но так же и показательный пример того, как следует выделить предметную область в программном обеспечении, для того, чтобы проще преодолевать сложности, частые изменения, проблемы коммуникации и прочие недуги предметной области, вместо того чтобы разрабатывать уродливую, сложную для понимания систему, в которой любое изменение или исправление способно обрушить на вас лавину всё новых и новых дефектов.
DDD ни в коем случае не отрицает наследия практик разработки, таких как:
Шаблоны проектирования (Design Patterns) (в том числе всем известные GoF)
Так называемый ряд принципов проектирования S.O.L.I.D, собранных Робертом “Uncle Bob” Мартином.
DDD лишь дополняет их. Поиск подходящей модели и абстракций в сложных сценариях требует значительных знаний в сфере объектно-ориентированного подхода, и достаточного опыта применения различных принципов, шаблонов и практик, а не просто DDD, как может показаться.
3. С чего можно начать?
Если мой “нудный PR” проектирования на основе предметной области (DDD) вас до сих пор не утомил, то думаю нам стоит продолжить, если же иначе, то посмотрите хотя бы ссылки на материалы.
Первой книгой пролившей свет на DDD для широкой публики была так называемая “Большая синяя книга” (мем. BBB: Big Blue Book): Domain-Driven Design: Tackling Complexity in the Heart of Software byEric Evans (на русский язык пока не переведена).
Книга довольна подробно рассказывает о том, что из себя представляет DDD, и все связанные аспекты, такие как: язык предметной области, шаблоны, практики проектирования, рефакторинг, моделирование, как сделать разработку гибкой и многое другое. Но даже если вы ознакомитесь со всеми вопросами, поднятыми в книге (что является не совсем простым занятием), вы обратите внимание, что вопросы рассматриваются только с теоретической точки зрения, оставляя весь простор для практики (книга не привязана к конкретной платформе разработки). Для большинства из нас чтение чистой теории, без подкрепления практическими примерами не нравится, в связи с этим можно обратить своё внимание на сокращенную (и свободную для доступа) версию этой книги, подготовленную порталом InfoQ: Domain Driven Design Quickly.
Есть так же несколько хороших презентаций Эрика Ивенса (Eric Evans), с которых можно начать:
На портале InfoQ можно найти множество других презентаций, статей и интервью, посвященных DDD.
Итак, с теоретической частью мы разобрались, где же можно найти примеры практического применения DDD? Отличной книгой для этого является .NET Domain-Driven Design with C#, Problem – Design – Solution написанная Tim McCarthy.
В этой книге вы найдете практические примеры:
1) Как проходит процесс проектирования и разработки, от определения требований, до написания кода
2) Как организовывать архитектурные слои в своих решениях
3) Как применять шаблоны и практики DDD
4) Как построить небольшой каркас для DDD
5) Как изолировать домен предметной области от модели
6) Современные паттерны представления данных и взаимодействия с ними (Model-View-ViewModel) в такой среде как WPF (так же применимы к Silverlight) в практики.
Эта книга – отличный практикум по DDD, содержащий очень широкий пласт идей. Начинается книга с разработки требований, а заканчивается реализацией промышленного приложения, исходные коды которого доступны на Codeplex.
Вся концепция книги построена на 3 книгах-столпах DDD:
В этой книге поверхностно рассмотрены все вопросы, техники и паттерны, применяемые в DDD, все примеры сопровождаются кодом, что упрощает понимание. Книга превосходная, однако русский перевод подкачал, поэтому, рекомендую прочитать оригинал.
Однако DDD – это не просто практические решения или шаблоны, это мышление и подход, и есть великое множество нюансов, которые необходимо учитывать, если вы решили следовать DDD, таких как: фокусирование на высокий приоритет отдается модели, выработка языка предметной области, контекст модели, процесс моделирования, разделение знаний, рефакторинг, стратегический дизайн и т.д…это является основной причиной ознакомиться с книгой Эрика Ивенса, так как она даст вам более объемное и глубокое понимание философии DDD.
DDD не привязанны к конкретной технологии, однако соблюдать DDD будет не так просто, без наличия хороших средств и практик в вашем арсенале, таких как: TDD-фреймворк, ORM, возможность реализации независимости сохраняемости (Persistence Ignorance), IoC-контейнер (Inversion of Control), и возможностей AOP (Аспектно-Ориентированного Программирования), конечно не значит, что все эти инструменты нам понадобятся, однако они приблизят нас к реализации DDD на практике. Практичная ценность этих средств в том, что они позволять изолировать модель предметной области, что является ключевой целью DDD. Книга Джимми Нильссона может познакомить вас с возможностями и видами данных инструментов. Джимми так же показывает как использовать шаблоны реализации корпоративных приложений, и строить, благодаря им, цельное решение, основанное на современных инструментах и практиках.
Некоторые реализации шаблонов DDD на Ruby On Rails:
Some DDD (Domain Driven Design) Concepts implemented in Rails
4. Актуальные вопросы DDD
C DDD так же тесно связана такая тема, как DDDD: Distributed Domain Driven Design (Распределенный DDD). DDDD – это DDD в распределенных сценариях. В настоящее время существует не так много ресурсов, посвященных DDDD, в нескольких словах о DDDD: покрывает проблему реализации сообщений и DDD, разделение команд и запросов (Command Query Separation (CQS)) помогает реализовать данный подход. Грег Янг (Greg Young) сообщил, что готовит книгу, посвященную DDDD.
SOA и DDD – это ещё одна объемная тема, часто обсуждаемая Udi Dahan
5. DDD шаблоны, концепции и понятия
В промышленных приложениях DDD использует ряд шаблонов, часть которых описана в книге Эрика Ивенса, но, это не отменяет применение объектно-ориентированного подхода, включающего GoF-шаблоны, шаблоны Мартина Фаулера, описанные в его PoEAA, Шаблоны интеграции корпоративных приложений и т.д.…
Вот некоторые из них:
6. Примеры приложений
Найти хорошие примеры реализации DDD очень сложно, не потому что их не существует, а потому что реальная сила DDD реализуется в приложениях, используемых в довольно сложных областях, и, как правило, являющихся коммерческими проектами. Однако можно найти несколько неплохих проектов, в которых можно проследить некоторые идеи реализации шаблонов DDD.
1) Приложение Тима Маккарти его проект, описанный в деталях в его книге. Он описывает не только применение шаблонов, но так же акцентирует внимание в разработке модели предметной области с точки зрения DDD.
2) Следующий проект, на который следует обратить внимание – это приложение разработанное Yves Goeleven, создание данного приложения описано в его блоге (так же посвященному основным концептам DDD). Другим его приложением является DDD-каркас. Следует обратить внимание на его реализацию взаимодействия шаблонов Repository и Specification.
3) Billy McCafferty разрабатывает потрясающий open source фреймворк, сфокусированный на DDD, под названием S#arp Architecture. У него есть очень хорошее описание, включающее в себя описание шаблонов и подходов, заключенных в фреймворке. Фреймворк нацелен на разработку ASP.NET MVC приложений с применением NHibernate.
4) C# Domain-Driven Design sample application ( ndddsample ), это приложение, разрабатываемое Джимми Нильссоном, демонстрирует разбиение приложения на ключевые слои с точки зрения DDD. Так же демонстрируется практическое применение шаблонов building block в предметной области перевозки грузов, описанной в его книге.
Этот проект основан на совместной работе компании Эрика Ивенса “Domain Language” и шведской консалтинговой компании “Citerus”.
Изоляция модели предметной области
Эта статья является переводом материала «Domain model isolation».
Термин «изоляция модели предметной области» уже давно используется, но его значение может быть не таким очевидным, как многие думают. В этом посте автор оригинала попытается описать, что значит правильно изолировать модель предметной области и почему это важно.
Изоляция модели предметной области
Однако это еще не все, и понятие иммутабельного ядра в функциональной архитектуре не полностью соответствует модели изолированной предметной области. Другими словами, модель предметной области может быть полностью изолированной, но в то же время не полностью чистой в функциональном смысле. Возможна и обратная ситуация:
Главное различие между ними заключается в том, как они относятся к иммутабельности. В то время как в функциональном программировании побочные эффекты, как правило, полностью удаляются из иммутабельного ядра, DDD в целом не запрещает их, если они ограничены границами модели предметной области.
Другими словами, операция, вызываемая в модели предметной области, может изменять состояние объектов в ней, но сама по себе не должна приводить к изменению где-то в файловой системе или в стороннем API. Вот для чего нужны сервисы приложений. Модель предметной области отвечает за принятие бизнес-решений, в то время как уровень сервисов приложений преобразует эти решения в видимые биты, такие как изменения в БД.
Что делает доменную модель изолированной?
Здесь, например, связь между Person и Address не противоречит концепции изоляции модели предметной области. Однако вы также можете видеть, что сущность Person ссылается на соответствующий репозиторий, а объект значения Address использует API местоположения. Оба этих класса являются шлюзами во внешний мир, и, таким образом, взаимодействие с ними нарушает изоляцию модели предметной области.
Отношения, которые вы видите на этой диаграмме, направлены вверх, от внутреннего ядра лука к внешним слоям. В то же время луковая архитектура говорит нам, что все коммуникации должны идти только в обратном направлении, от верхних слоев к внутренним. Нарушение этого правила говорит нам об утечке модели предметной области.
Изолированная модель предметной области является закрытой. Все операции над моделью домена должны быть закрыты в соответствии с ее сущностями и объектами значений. Другими словами, аргументы этих операций, явные или неявные, и их возвращаемые значения должны состоять только из примитивных типов или самих классов предметной области.
Вот пример, иллюстрирующий эту идею:
Объект значения Address принимает адресную строку и использует Location API для поиска соответствующих почтовых индексов. Класс LocationApi здесь является неявным аргументом конструктора. Это не доменный класс и не примитив. Следовательно, данная реализация модели предметной области не замкнута сама по себе, не изолирована.
Обратите внимание, что понятие изоляции применимо только к сущностям и объектам значений. Они являются сердцем любой модели предметной области. Другие доменные классы, такие как фабрики, доменные службы и, очевидно, репозитории, могут и обычно ссылаются на внешний мир.
Изоляция модели предметной области и внедрение зависимостей
Давайте посмотрим на обычную практику решения проблемы изоляции. Иногда автор оригинала видит, как люди соглашаются с тем, что следующий код имеет запах:
И тут же предлагают «исправить» этот запах введя интерфейс:
Вы, конечно, можете применить практику внедрения зависимостей в модель предметной области, но эти зависимости должны представлять собой реальную абстракцию, значимую для вашего домена, а не что-то исходящее из внешнего мира. Во многих случаях это означает, что интерфейсы, которые вы вводите в свои сущности и объекты значения, должны быть реализованы другими сущностями и объектами значениями. Кроме того, принцип повторно используемых абстракций говорит нам, что для того, чтобы эти интерфейсы считались хорошей абстракцией, они должны иметь более одной реализации. Некоторые программисты даже предполагают, что для каждого интерфейса должно быть минимум 3 класса, реализующие их.
Это, кстати, предпосылка, лежащая в основе принципа инверсии зависимостей (DIP). Хотя вы можете легко реализовать внедрение зависимостей, используя некоторые произвольно выбранные интерфейсы, эти интерфейсы не следуют автоматически принципу DIP.
Все это вместе делает хорошо спроектированную изолированную модель предметной области маловероятной, чтобы у нее были какие-либо интерфейсы, «абстрагирующие» сущности и объекты значений. Сущности и объекты-значения сами по себе очень хорошо представляют предметную область, обычно нет необходимости их дальше абстрагировать.
Загрязнение семантики
Причина в том, что такие внешние концепции почти всегда содержат данные, которые не нужны вашему основному домену. Поэтому эти данные должны быть отфильтрованы, чтобы соответствовать принципу YAGNI.
Обратите внимание, что наличие антикоррупционного слоя, хотя и полезно во многих случаях, не всегда оправдано, и ваша модель предметной области вполне может находиться в партнерских отношениях с другими ограниченными контекстами или даже иметь с ними общее ядро.
Зачем изолировать модель предметной области?
Вышеупомянутые рекомендации требуют много работы, поэтому можно задать следующий вопрос: зачем вообще беспокоиться? Зачем прикладывать столько усилий?
Ответ, как всегда, заключается в том, чтобы бороться со сложностью. Правильное разделение проблем позволяет снизить когнитивную нагрузку, необходимую для размышлений о кодовой базе. Чем сложнее становится ваш проект, тем важнее становится этот вопрос. При достаточно больших базах кода очень важно иметь возможность думать о модели предметной области в отрыве от всех других проблем. Во многих случаях это фактически единственный способ справиться с постоянно растущей сложностью вашего проекта.
Резюме
Понятие изоляции модели предметной области применимо только к сущностям и объектам значений. Другие классы предметной области могут общаться с внешним миром.
Эта концепция похожа на понятие неизменяемого ядра из функциональной архитектуры. Основное отличие состоит в том, что изоляция модели предметной области допускает побочные эффекты, которые ограничены границами этой модели.
Замена внешней зависимости интерфейсом не означает, что вы исправляете утечку доменной модели. Чтобы придерживаться принципа инверсии зависимостей (DIP), интерфейс должен представлять значимую для бизнеса абстракцию в том смысле, что он должен помогать классам предметной области принимать бизнес-решения.
Изоляция модели предметной области дает два преимущества: она снижает сложность кода и обеспечивает лучшую тестируемость.
Блеск и нищета модели предметной области
Мартин Фаулер в книге «Patterns of Enterprise Application Architecture» описывает «Модель предметной области (Domain Model)» как сложный подход к организации бизнес-логики. Метод заключается в создании классов, соответствующих объектам предметной области из реального мира как с точки зрения структуры данных, так и поведения. При этом технические аспекты, такие как хранение данных, аутентификация и авторизация, управление транзакциями, выносится за пределы слоя бизнес-логики. Паттерн реализуется одним из двух способов:
Структура поста:
Историческая справка
Впервые я столкнулся с термином «модель предметной области» (Domain Model) читая книгу Patterns Of Enterprise Application Architecture (PoEAA) Мартина Фаулера. Прочитал и немного чего понял. Может быть время было неподходящее, а может у Фаулера было написано уж очень кратко. Так или иначе, после прочтения паттерн был забыт на несколько лет, пока в руки ко мне не попала небезызвестная «синяя книга» Эрика Эванса Domain Driven Design. На этот раз я по настоящему проникся, как в фильме «Матрица», когда Нео смогу наконец освободить свой разум. Не в том смысле, конечно, что начал останавливать пули силой мысли или обрел какие-то иные сверхъестественные способности. Вместо этого в значительной изменились мои взгляды на то, что важно, а что не очень в контексте разработки корпоративного ПО. До прочтения я считал единственно-важным технологический аспект, а после — закрались мысли, что нужно еще заниматься аналитикой, сбором требований и другими немаловажными вещами, а главные сложности в корпоративной сфере вообще связаны с людьми, а не технологиями.
Однако, далеко не все разделяют мой щенячий восторг по поводу Эванса, многим больше нравится «красная» книжка Вернона. На вкус и цвет, как говорится, все фломастеры разные, так что какую из них читать каждый решает сам. Можно читать и обе, но их содержание в значительной степени пересекается. А вот книга Скотта Влашина Domain Modeling Made Functional вышла буквально в прошлом году. Она примечательна тем, что рассматривает типовые проблемы предметно-ориентированного проектирования через призму функционального программирования и дает некоторые неожиданные ответы, недоступные в ООП.
Например, в ООП исторически сложилось два основных подхода к моделированию домена: «богатая» и «бедная или анемичная» модели. В ФП же, этого разделения нет, потому что функциональные языки отличаются по возможностям от классических ООП-языков и потому что в функциональном мире вообще не принято объединять структуру данных и операции над ними (поведение) в объекты. В контексте доклада я буду в основном оставаться в объектно-ориентированной парадигме и лишь пару раз соскользну на кривую тропинку функциональщины.
Богатая
Богатая модель — это способ моделирования, который имели в виду Фаулер и Эванс:
Анемичная
В анемичной, все ровно наоборот.
Причины раскола
Сложно сказать в какой именно момент произошел этот раскол и что стало тому причиной. Лично я считаю, что основных причин две:
Засилье ORM
Давайте посмотрим на статистику скачивания пакетов с nuget.org. Entity Framework скачивают чаще, чем ASP.NET MVC. Можно предположить, что множества скачивающих ASP.NET MVC и Entity Framework в значительной степени пересекаются, и сделать вывод, что многие веб-приложения манипулируют данными посредством ORM. На сколько это действительно нужно делать — вопрос открытый.
Простота реализации
Анемичную модель, наверняка хотя бы раз в жизни реализовывал каждый из читающих этот текст. Как же правильно реализовать богатую модель — вопрос гораздо менее однозначный. Несмотря на обилие теоретического материала как только дело доходит до реализации на практике, появляются вопросы, на некоторые из которых я не нашел внятного ответа до сих пор.
Always valid (миф или реальность?)
Можно предположить, что проблема кроется исключительно в недостаточной квалификации программистов. Почему же тогда таких «неумех» так много в индустрии? Почему люди отказываются от столь заманчивых идей, как «писать код, из которого понятны бизнес-правила» и «всегда соблюдать инвариант»?
Ведь что значит стопроцентное соблюдение всех инвариантов? В пределе — то, что ни один объект вообще нельзя создать в «неправильном состоянии». А это значит, что ошибки будут найдены не на этапе юнит-тестирования, приемочного тестирования или, упаси господи, на продакшене, а в момент компиляции.
Более глубоко тема инвариантов и проектирования, направленного на исключение возможности ошибки во время компиляции программы, а не во время выполнения в статье Скотта Влашина «Making illegal states unrepresentable»
Кто вообще в здравом уме будет отказываться от такого? Не жизнь, а сказка… или миф… а может быть художественный вымысел?
Такая дискуссия состоялась в интернете между Грегом Янгом и Джеффри Палермо. Первый — сторонник концепции Always Valid, а второй утверждал, что этот подход вообще неосуществим в реальности.
Аргументы Джеффри вполне логичны. Для обеспечения корректности состояния любого объекта в изменяемой (mutable) среде нам придется снабдить любой setter защитной конструкцией, например такой.
Такой подход не только сильно замусоривает и зашумляет код, но и действительно не очень дружит с SOLID, потому что такие классы получают две причины для изменений: хранение данных и валидацию. Чтобы не нарушать принцип единственной ответственности мы могли бы объявить отдельный интерфейс валидатора, реализовать его и перенести логику валидации в соответствующую реализацию.
Давайте оставим пока SOLID в покое и посмотрим на эту аргументацию с другой стороны. В некоторых случаях обеспечить корректное состояние объектов может быть просто невозможно. Коммерсанты давно смекнули, что вероятность покупки в интернет магазине снижается в зависимости от количества необходимых форм для заполнения. В идеале вообще должна быть только одна огромная кнопка «купить» и какой-то способ связаться с пользователем. Остальные «необходимые» поля может заполнить кол-центр после получения оплаты.
С другой стороны, чем больше магазин знает о вас, тем лучше можно настроить маркетинговые кампании, чтобы показать более релевантную рекламу, которая с большей вероятностью заставит вас пойти на сайт магазина покупать что-то снова и снова.
Таким образом, в одних случаях нужно, чтобы обязательных полей было минимум, а в других — максимум. Значит в разных контекстах правила «обязательности» полей класса могут быть разными и экземпляр класс вообще не может всегда находиться в «правильном» состоянии, потому что «правильность» зависит от текущего контекста. Шах и мат, Грег Янг?
Проблема универсалий
Удивительно, что впервые в известной человечеству истории подобными вопросами задались Платон с Аристотелем задолго до становления кибернетики с информатикой и изобретения компьютеров. Я не буду сильно углубляться в онтологию, да просят меня древнегреческие философы за весьма вольное толкование их идей.
Представьте себе единорога. Не конкретного, а единорога в принципе, как абстрактную концепцию. А теперь представьте объект, относящийся к классу «единорогов». Здесь термины объект и класс я трактую широко: не в смысле терминологии ООП, а как категорию, в которую входят всевозможные единороги и одного представителя этой категории. Считаете что на картинке слева единорог? А справа?
Здесь не все так очевидно. Это персонаж мультфильма «Гадкий я 3», на протяжение которого одна маленькая девочка искала живого единорога, потому что единорог слева был ее любимой игрушкой… и в общем и целом она нашла. Какое отношение древнегреческие философы и современные мультфильмы имеют к контекстной валидации? Как ни странно, самое прямое.
Контекстная валидация и инвариант
Давайте разделим валидацию на два подвида:
Дальше я буду сравнивать богатую и анемичную модель и присваивать одно очко богатой, если реализация богатой модели оказалась такой же простой, как и анемичной, и одно очко анемичной, когда реализация богатой модели окажется неудобной или неполной. Простота реализации против надежности удобства сопровождения.
Паттерны DDD на практике
DDD-жаргон
Прежде чем начать соревнование, напомню DDD-жаргон. Модель не монолитна, а разделена на несколько ограниченных контекстов. Вопрос соотношения терминов домен, субдомен и ограниченный контекст я оставляю за скобками, потому что он не важен в рамках доклада. Существование ограниченных контекстов объясняется организационными причинами. Чаще всего невозможно создать единую модель для всего предприятия, потому что такая модель не будет отражать реальную неоднородную структуру компании, разнящейся от отдела к отделу.
Скажем, решили вы заказать изготовление продукции. Пока вы ее не оплатите никто не поднимет пятой точки. Зато после оплаты заявка поступает в отдел производства. Отделу производства в свою очередь не важно оплачена заявка или нет. Для них актуальны сроки выполнения и наличие необходимых материалов на складе. Затем товар отправляется в доставку, которому вообще по барабану что это за товар. Их волнует только расстояние от склада отгрузки до точки доставки.
Таким образом, существует три заявки: на оплату, изготовление и доставку, обладающие разными характеристиками и имеющие смысл только внутри ограниченного контекста, а не всего предприятия. Словарь терминов, понимаемый одинаково в рамках ограниченного контекста называется единым языком. Внутри ограниченного контекста DDD предлагает три основных инструмента моделирования: Value Object, Entity и Aggregate.
Агрегаты — это деревья объектов, обладающие инвариантом для группы, а не для единичного объекта. Доступ к агрегатам осуществляется через «Корень агрегации» — объект, находящийся в корне дерева. Таким образом, корень обеспечивает инвариант всей группы с помощью инкапсуляции.
Сущности и Value Object — это основные строительные блоки приложения, которые могут как входить в агрегаты, так и не входить. Их основное отличие в том, что у сущностей есть уникальный идентификатор, а у объектов-значений — нет.
Дизайн на основе типов
Вернемся к пользователю интернет магазина. Попробуем смоделировать всего один объект в стиле «богатой» модели. Мы пришли к тому, что валидацию инварианта и контекстную валидацию необходимо разделить. Самый простой способ достижения цели — разделить класс, моделирующий пользователя. Напрашивается два основных подтипа:
Подробнее этот подход в докладе Скотта Влашина Domain Modeling Made Functional или нашего с Вагифом Абиловым Жизнь после бизнес-объектов.
Вынести IO на границы (Anticorruption Layer)
Следующим шагом перенесем ввод-вывод на границы приложения. Данные, пришедшие извне по определению могут быть в любом состоянии: как в согласованном, так и нет. DDD даже предлагает специальный паттерн для пограничного контроля ограниченных контекстов — Anticorruption Layer, который, впрочем, отличается от обычного фасада лишь более узкой специализацией.
Все данные из-за пределов контекста сначала попадают в фасад, где происходит проверка. Некорректные данные отвергаются, а данные, прошедшие валидацию идут дальше в слой домена. Над ними выполняются операции бизнес-логики. Результаты покидают пределы слоя домена. Новые входные данные даже на базе результатов «чистого» слоя домена все-равно считаются «грязными» и операция повторяется.
Crud у всего есть начало
Таким образом в домен попадают только корректные данные. Сам же слой домена соблюдает все инварианты, поэтому входные и выходные параметры всех операций должны быть всегда согласованными. Первый рубеж, гарантирующий корректность — конструкторы классов. Конструкторы изначально были задуманы для того, чтобы создавать только согласованные объекты, но долгое время ORM и сериализаторы умели работать только с непараметрическими конструкторами. Кроме того, синтаксис конструкторов в C++ подобных языках оказался чересчур многословным. В итоге, мы можем наблюдать противостояние прививочников и антипрививочников тех, кто считает, что конструкторы нужны и полезны и тех, кто считает, что это слишком многословно.
Проблема инициализации параметров хорошо решена в TypeScript с помощью parameter properties. В C#9 нам обещают records. Эта функциональность планировалась еще в C#8, но разработчики языка решили доработать концепцию и, похоже, что в следующей версии языка мы все-таки их дождемся.
В случае богатой модели выбора нет, конструктор должен быть. Контактные данные сделаем обязательным полем, чтобы использовать их в качестве уникального идентификатора, а профиль пользователя — необязательный параметр конструктора.
Контактные данные — это либо email, либо телефон. Необязательно заполнять и то и другое, кол-центр устроит любой способ связи, главное, чтобы он был заполнен верно. Для email — наличие собаки и домена в адресе, а для телефона знака «+» и следующих за ним цифр. Более точные правила валидации email и телефона намеренно опущены, потому что они сейчас не важны.
Мы могли бы использовать вот такой конструктор, но в C# нет способа показать, что один из параметров является обязательным. Сигнатура метода будет сообщать о том, что оба параметра не обязательные. Поэтому сделаем конструктор закрытым, а вместо него предоставим два публичным метода с более говорящими названиями. В C# обычно используется префикс Try для операций, которые могут завершиться ошибкой, но не выбрасывают исключений. Можно реализовать конструктор таким образом.
Если не хотите использовать TryPattern, можете использовать атрибуты. Несмотря на то, что атрибуты — это мета-информация, вообще никак не влияющая на исполнение программы, существует уже готовое вполне удобное API, использовав которое в конструкторе мы заставим пройти все проверки.
Откуда берутся пользователи
На этом можно было бы остановиться, если бы за пару простых действий нельзя было сделать бизнес-правила более отчетливыми в коде программы. Из сигнатуры конструктора не ясно при каких обстоятельствах пользователи появились в системе. Заменим два параметра на один и дадим ему понятное название.
Теперь стало ясно, что пользователь может зарегистрироваться самостоятельно (SignUp) или зарегистрироваться по приглашению друга (SignUpByInvite). Механизм приглашений может натолкнуть читающего код на мысль о том что в системе существует реферальная программа. У этого изменения есть еще один неожиданный побочный эффект. Представьте, что в логах есть два разных сообщения об ошибке:
Поверья
Я не зря упомянул раньше ORM и сериализацию. Встречаются программисты, считающие, что и сейчас конструкторы с параметрами не поддерживаются. Медленно, но поддержка добавляется. В случаях, когда ORM не справляется с параметрическим конструктором всегда остается план B. Оставить конструктор без параметров приватным или защищенным (в зависимости от того, будете ли вы использовать прокси) и добавить необходимые публичные конструкторы.
ORM будет пользоваться конструктором без параметров несмотря на модификаторы доступа, а в программном коде придется использовать публичные.
К сожалению модификаторы доступа не защищают от коллег, меняющих доступ конструктора без параметров на public. Это вопрос проведения код-ревью, а не архитектуры системы.
Также, конструкторы не поддерживают async/await. Этот вопрос хорошо разобран в статье Марка Симана Asynchronous Injection.
В некоторых случаях вместо публичного конструктора может лучше подойти фабричный метод, реализующий TryPattern. Использовать ли исключения для ошибок бизнес-логики вопрос неоднозначный. Подробнее об этом в статье Об ошибках и исключениях.
Подведем первые итоги. Я считаю, что счет 1:0 в пользу богатой модели. Совершив несколько тривиальных преобразований мы улучшили читаемость кода и сделали бизнес-правила явными повысили надежность и удобство сопровождения программы. Перейдем к более сложным сценариям. Пока мы работали только с сущностями и value object. Как дела обстоят с агрегатами? Как будет обеспечиваться инвариант целой группы объектов?
Агрегаты
Классический пример агрегата — заказ в интернет магазине. Если заказ оплачен и доставлен, поздно добавлять в него новые товары. Поэтому список товаров в заказе не может быть публичным, иначе любой программист сможет воспользоваться этим свойством и добавить товар, несмотря на статус, и будет при этом абсолютно прав, потому что сигнатура класса никак не сообщила ему о зависимости между состоянием заказа и товарами в нем.
Столь же классическое решение этой проблемы — предоставить во вне readonly-список, а операции записи осуществлять с помощью специализированных методов, содержащих необходимые проверки. И с этого момента начинаются первые проблемы.
Паттерн Агрегат — очень хорошо выглядит на бумаге, но оказывается весьма неуклюжим, когда дело доходит до практики. Агрегаты по своей природе сложнее чем сущности или объекты-значения, потому что представляют собой не один объект, а целое дерево. Поэтому проблемы, которые казались на уровне сущностей незначительными, становятся весьма неприятными по мере роста дерева объектов и возможных комбинаций состояний. Рассмотрим типовой сценарий работы с заказом: проверка наличия на складе, отправка, отмена.
Нужно ли проверять наличие на складе до оплаты? Зависит от требований. Можно ли отправлять заказ до оплаты? Некоторые магазины разрешают оплачивать при получении курьером, правда чаще всего только если доставка осуществляется внутри города. Можно ли отменить заказ, удачно поставленный покупателю. Скорее всего в нашем дизайне не хватает статусов и лучше подойдет не «отменен», а новый статус «разбирательство», открывающий целый новый процесс: то ли мы разбили товар во время доставки, то ли на складе что-то перепутали, то ли покупатель что-то напутал и ему привезли ровно то что он заказывал.
Данные и методы, связанные с соответствующими состояниями заказа имеют смысл, только в рамках одного состояния. Мы же снова объединили все в одном классе и получили пусть и вкусный, но все-таки винегрет. Поэтому счет сравнялся — 1:1. Напоминаю, анемичная модель никогда не утверждала, что будет соблюдаться инвариант, тем более для группы объектов и не обещала того, что из программного кода будут понятны бизнес-правила, поэтому в рамках анемичной модели претензий к дизайну нет, а к богатой появились вопросы. Где выразительность? Где статический анализ для бизнес-правил? Его нет, по крайней мере в классических объектно-ориентированных языках. Зато такая возможность есть в функциональных ЯП с более сильной системой типов.
В F# все иначе
В F# существуют так-называемые алгебраические типы данных: records (да-да, те самые, что завезут в C#9) и discriminated union.
Поддержки discriminated union в C# в ближайшее время не планируется. Можно воспринимать их как enum на стероидах. В отличие от классического enum-а в перечислении discriminated union могут входить другие типы, в т.ч. и records. Именно поэтому, такая система типов называется «алгебраической». Record — это тип «и &», а discriminated union 0 это тип «или |». Таким образом все приложение может быть построено за счет комбинирования маленьких типов одним из способом. В отличие, от привычного в ООП control flow, основанного на полиморфизме, в ФП часто используется передача управления с помощью pattern matching. Для каждого подтипа в discriminated union необходимо написать свою ветку выполнения, в которой будут доступны только данные и поведение, имеющее смысл в рамках в данного состояния объекта.
Эта проблема решается и без применения F#, однако в C# решение выглядит менее элегантным. В статье Шаблон проектирования «состояние» двадцать лет спустя имитируется функциональный подход на основе классического ООП-паттерна с применением современных языковых конструкций C#. На момент написания статьи switch expression еще не зарелизили. С ним pattern matching выглядит лучше. Проконтролировать разбор всех наследников можно написав свой анализатор Roslyn.
Несмотря на то что мне пришлось сменить язык программирования, я считаю что можно выдать богатой модели еще одно очко. Счет становится 2:1. F# совместим с C# и поддерживает объектно-ориентированную парадигму, поэтому используя только F# или F# в сочетание с C# можно решить проблему разного поведения в разных состояниях. К сожалению, радоваться еще рано. У меня в запасе есть еще несколько сценариев проблематичных сценариев.
Домен и инфраструктура
Распределенные транзакции
«Классическое фаулеро-эвансвое» DDD настаивает на том, что инфраструктура и домен должны быть разделены. А что делать, если инфраструктура становится частью домена. Как так? Легко. Например системы документооборота. Представьте, что вам нужно загружать и подписывать цифровыми подписями, а затем парсить и работать с данными из сотни тысяч документов. Каждый раз открывать бинарные файлы — не вариант. Поэтому вам потребуется механизм, гарантирующий консистентность данных в бинарных файлах и в реляционной структуре при добавлении новых или редактировании старых документов.
В основе таких гарантий лежит обработка ошибок, как связанных с системой хранения файлов, так и с БД. Если в момент обновления первого или второго происходит ошибка, то выполняется компенсация — удаляется загруженный файл или откатывается транзакция к БД. Да, существуют механизмы распределенных транзакций. Если бы они работали на любой инфраструктуре, с любыми хранилищами данных проблема бы не существовала. К сожалению это не так, и довольно часто приходится писать код, специфичный для конкретного проекта и его инфраструктуры.
Internal
Можно делегировать это разработчикам, но где гарантия того, что каждый из разработчиков окажется достаточно ответственным и не забудет что-то обработать? Мой опыт показывает, что таких гарантий дать нельзя. Счёт сравнивается 2:2.
Я считаю, что единственный способ что-то гарантировать — это отобрать возможность выбора. Если public заменить на internal, положить эти объекты в одну сборку, а публичный API предоставить через специализированные методы сервисов, то куда вы денетесь с подводной лодки?
Сервисный слой
Для обновления документа мы:
Теперь все будут использовать именно этот метод. Я предпочел бы этот страшный зашумлённый код никогда больше не писать и оставить здесь — чтобы он был только в одном сервисе, и ключевое слово internal нам помогает ровно так и сделать.
Слой сервисов в доменной модели гораздо более тонкий и служит для специфических задач. Он будет полезен не для всей бизнес логики, а, например, для коммуникации с инфраструктурой.
Все вместе
Главный вопрос DDD, смысла жизни и всего такого?
Вы хотите получить банан, но получаете гориллу, которая держит в руках банан и вместе с ней все джунгли впридачу.
Слышите иронию над неявным изменяемым состоянием, которое присуще ООП? Если вы работаете с объектами по ссылке, тем более с интерфейсными ссылками, и не знаете настоящие реализации, вы не знаете сколько будет внешних ссылок. Так и получаются те самые «джунгли».
Действительно, 2:3. Большие агрегаты падают с OutOfMemoryException, сделать ничего нельзя. По крайней мере в объектной парадигме.
Если только вы не решите, что в стеке чтения вы не очень-то хотите это всё загружать. А в стеке чтения вы довольно часто не хотите ничего загружать. Поэтому в мире ООП ответ на этот вопрос несколько другой, он звучит так: напишите SQL-запрос.
До этого я рассказывал, что необходимо делать правильную модель домена, нельзя ни в коем случае писать никаких SQL-запросов, всё должно быть объектно, и тут я говорю: «Давайте напишем SQL-запрос». Не то, чтобы я переобуваюсь на ходу, просто DDD — это инструмент. Моделирование домена — это инструмент, паттерн. Не бывает швейцарских ножей, которые работают во всех обстоятельствах. Отчёты — это плохое применение для DDD.
Read-stack — это зачастую плохое применение для DDD, потому что нам нечего там контролировать. Нам надо просто читать данные.
Здесь я опускаю важный аспект предоставления доступа к данным. Конечно нужно «не просто» читать. Если вы используете ORM в read-stack, вас может заинтересовать статья Доступ к данным в многопользовательских приложениях.
А вот в стеке записи это вполне подходящая штука. Кроме того, CQRS, в принципе, некий симбионт для HTTP, потому как протокол HTTP явно говорит о том, что методы POST и DELETE должны менять состояние сервера, но не возвращать данные, а метод READ — читать. Соответственно, если ваше приложение в вебе, то почему бы не воспользоваться такой возможностью.
По наклонной («плохие» случаи)
Делаем не SOLIDно
Перейдём на уровень «похуже» и ответим для себя на вопрос: насколько мы вообще ценим SOLID? Если принцип D вам не очень близок, и вы не очень знаете, зачем абстракциям зависеть от абстракции или реализации, и вообще вам всё равно — отлично, просто засовываем сюда IQueryable и не паримся. Да, мы нарушили принцип — ну и что?
Если вы считаете, что при использовании LINQ вы ничего не нарушаете, попробуйте заменить лямбду x => x.OrderId == Id на любую другую и скажите — выполняется ли здесь принцип L? Если вы уверены, что принцип L здесь всегда выполняется — у меня для вас плохие новости. Это я к тому, что любая абстракция при определённых условиях начинает течь. Зависит от того, насколько ваше пуританское воспитание позволяет или не позволяет так делать.
Lazy Load
Вариант «ещё похуже» — я двигаюсь к абсолютному злу — включите Lazy Load. Он по многим причинам хорош:
Проектирование — компромисс
Я уже, кажется, раза два повторил, что не бывает идеальных инструментов: всегда есть компромисс, и проектирование — это компромисс. С моей точки зрения, это настолько важный note point, что я его ещё раз повторю: если у вас никак не получается сделать DDD в read-stack, это не говорит о том, что вы не смогли в DDD, и не говорит о том, что DDD плохой — это говорит о том, что DDD плохой инструмент для этой задачи, поэтому просто возьмите другой инструмент.
Как только вы начинаете возводить всё в абсолют и говорить «нет, мы не будем писать так код, потому что Фаулер Эванс, Вернан — кто угодно — сказал, что „нельзя“, вы обязательно будете испытывать только расстройство. Только эти ребята (на слайде персонажи фильма „Звёздные войны“) возводят всё в абсолют. Поэтому, несмотря на все оговорки и не самое высокое качество тех решений, которые я предложил, давайте считать, что счёт у нас равный — 3:3.
Workarounds
Вернёмся к агрегатам: независимо от того, анемичную или богатую модель мы используем, большие агрегаты никуда не делись. То есть когда вы тащите половину базы данных, чтобы что-то посчитать, по умолчанию такой подход будет менее эффективным, чем просто выполнить запрос к базе данных. Ответить на рукописный запрос всегда будет эффективней, потому что не надо тащить данные по сети, поднимать их в оперативную память и там считать. База данных это делает внутри своего приложения. Значит ли это, что для определённого класса задач объектная модель никак не подходит? Если бы мы работали с Java, я бы сказал „да, так и есть“.
Expressions
Specification
Паттерн „Спецификация“ раньше работал только с объектами и создавал проблемы с производительностью, потому что нам нужно было сначала вытащить весь набор данных, а потом в оперативной памяти его отфильтровать. В C# этот паттерн обретает новую жизнь: если у нас есть правило о том, что для продажи только определённые товары, у которых цена больше нуля, мы можем объявить это правило как Expression.
Оно выглядит как C# код, соответственно, оно может быть частью нашей модели домена, но мы его никогда не выполняем как C# код — мы его используем для трансляции к запросу к базе данных. При этом мы получаем довольно эффективные запросы. Также если ваши объекты, агрегаты или сущности слишком большие, не обязательно их читать целиком, можно, используя C# и проекции, читать только часть этих данных в виде DTO и делать более производительные программы. Причем необязательно даже делать анонимные типы. Если вы используете AutoMapper или Mapster, можно вообще снизить количество императивного кода и заменить его на декларативный.
Default Interfaces Implementation
При этом возникают некоторые интересные лазейки, которые именно в классической ООП-парадигме не были бы возможными. Начнём с того, что с 8-ой версией C# у нас появились дефолтные реализации интерфейсов.
Давайте представим, что у товара есть логика расчёта скидки, которую мы поместили в тот же объект, то есть мы попытались сделать богатую модель. Такому проектированию сопутствуют все проблемы, о которых я говорил до этого: если нам потребуется вытащить большое количество товаров, то память неминуемо закончится. Мы бы хотели это оптимизировать: вместо того, чтобы объявлять метод непосредственно в сущности, мы можем перенести его в интерфейс с дефолтной реализацией.
Теперь этот интерфейс можно „прилепить“ как к сущности, так и к DTO. Непосредственно код будет находиться ни в том, ни в другом объекте, а в реализации интерфейса.
Bulk Extensions
А может F#?
Однако как только я начинаю использовать некоторые такие лазейки, я задумываюсь: на том ли языке программирования я сейчас пишу и тот ли инструмент я использую? Потому что дефолтная реализация интерфейсов — это, фактически, функция, которую мы можем „прилеплять“ к любым типам данных, используя интерфейс. Стоит ли переходить на F#, чтобы реализовать модель домена — каждый решает сам, исходя из потребностей проекта. Мы так и не решились переезжать. Тем не менее, поглядывать в сторону каких-то других языков программирования бывает полезно, чтобы позаимствовать оттуда некоторые идеи.
Bounded Context
Независимо от того, какая модель используется — богатая или анемичная, неплохо было бы разделять приложения на приложения поменьше, хотя бы просто потому, что ими проще управлять.
Ложный агрегат
Я говорил о том, что агрегаты имеют свойство разрастаться. Это происходит, когда мы используем документацию Microsoft и следуем ей вслепую. Например, когда мы хотим объявить связанные коллекции, мы обычно связываем их в две стороны. А вот если пользователь может много чего делать в вашей системе, тогда и объект User будет довольно-таки большим. Это антипаттерн „Божественный объект“.
И ладно, если бы он только тратил ресурсы памяти — это мы можем оптимизировать с помощью Expressions. Такой подход ещё и порождает большое количество циклических зависимостей, потому что сейчас у заказов тоже может быть набор пользователей, у комментариев может быть набор товаров, и так далее.
Пока все эти объекты живут у вас в одной сборке, вы получаете такие „круговые“ зависимости: A зависит от B, B зависит от C, C зависит от A. В итоге получается большой и страшный монолит, который мы никак не можем распилить. Всё дело в том, что этот агрегат ложный, в реальном мире его не существует, потому что я объединил в класс пользователя всё, что только можно.
Как же тогда находить корни агрегации и выбирать их правильным способом? Есть два списка вопросов, ответив на которые вы сможете с очень высокой вероятностью понять, тот ли у вас агрегат.
Первый список вопросов (организационный и никак не связан с технологиями)
Второй список вопросов (технологический)
Как делить Domain Model на Bounded Context
Как же делить Domain Model на Bounded Context? Я уже сказал, что в итоге внутри контекста у нас будут сущности и агрегаты. Сущности редко имеют тенденцию расползаться, потому что они маленькие, а пример „божественного“ агрегата я приводил до этого. И если вы видите, что агрегат залез между двумя контекстами, значит, он неправильный. Но это правило работает и в обратную сторону: если вы уверены, что агрегат правильный, и ответ на вопросы выше — „да“, то, похоже, вы неправильно нарезали контекст. Поэтому ответ на вопрос „как же нарезать Domain Model на Bounded Context?“ — по границам агрегатов.
Монолит на микросервисы
Этот вопрос можно переформулировать более модно: как делить монолит на микросервисы? Я не считаю, что разделение программы на подмодули вообще зависит от того, распределённая у вас система или нет. Распределённая система сопровождается некоторыми дополнительными проблемами, связанными с тем, что данные у вас находятся в разных процессах, и вам нужно постоянно что-то сериализовывать и десериализовывать.
Но, тем не менее, ответ такой: мы растащили Domain Model на разные Bounded Context, сказали, что Bounded Context — это отдельный микросервис и получили из страшного монолита много маленьких красивых микросервисов. Причём, обратите внимание, все проблемы, связанные со страшными монолитами, которые не распиливаются, чаще всего связаны с тем, что у вас есть циклические зависимости, которые сложно растащить. Если в самый начальный момент этих зависимостей нет, значит и разделить приложение на несколько процессов будет сильно проще.
Как пересечь границу контекстов
Что же делать, если из одного контекста всё-таки надо обратиться к другому контексту? Несмотря на то что они независимые, такое тоже бывает. Вне зависимости от того, разные ли это у вас сервисы или это разные сборки, работающие в одном процессе — в одну сторону бывает дотянуться проще, потому что если один контекст зависит от другого, то и ссылки на эти объекты есть. В другую же сторону уже не получится, потому что циклические зависимости, которые были в рамках одной сборки неявными, становятся явными. Компилятор уже не позволит нам ссылаться двумя сборками друг на друга.
Классическое решение такой ситуации — использование событий. События могут быть сериализованными и пересекать границы контекстов. Как именно „кидаться“ событиями — отдельная история. Если это микросервис, значит, вам потребуется, скорее всего, шина. Если это одно приложение, то это можно сделать и в памяти. Так или иначе, вы закончите тем, что у вас будет диспетчер, который будет слушать все эти события и направлять их по типу на разные обработчики. Если вам требуется реагировать на какие-то события из одного контекста в другом, то „выкидывайте“ события и обрабатывайте их в том контексте, в котором вам нужно.
Что выбрать?
Эволюционный рефакторинг
Тем не менее, если вы в начале проекта не совсем ещё поняли, будет он сложным или нет. Или вы не уверены, что ваш проект будет длиться 3, 6, 12, 24 месяца — по моим наблюдениям, это начинает давать результаты где-то после 3 месяцев — то, опять же, ничто не мешает просто начать с анемичной модели и реализовывать некоторые паттерны, когда они вам начинают требоваться. На первом этапе вполне разумно ограничиться следующим:
Полезные ссылки
Несмотря на то, что мы уже долго разбираем DDD, я рассказал, наверное 10% того, что вообще есть на эту тему. Поэтому я выбрал материалы, которые хорошо дополняют мой рассказ.
По-русски
In English
В этом году я буду MC (Master of Ceremonies) на конфереции DotNext 2020 Piter, которая пройдет с 15 по 18 июня. Благодаря онлайн-формату в этом году можно будет задать вопросы известным спикерам, таким как Scott Hanselmann и Jon Skeet.
Спасибо indienkova за помощь в подготовке материала. Без нее расшифровка могла не выйти или выйти значительно позже.