Про книгу Влада Хононова «Изучаем DDD — предметно-ориентированное проектирование»
Книгу Влада Хононова «Изучаем DDD — предметно-ориентированное проектирование» в первый раз я прочитал уже относительно давно — осенью прошлого года. Тогда же я собирался написать этот пост, и даже начал писать черновик, но что-то пошло не так и я переключился на другие посты и проекты. Через пару месяцев я решил все-таки дописать обзор этой книги, но предварительно решил еще раз освежить прочитанное и написать немного заметок в свою базу заметок. В итоге слишком увлекся, заметок понаписал много, книгу по сути перечитал еще раз, а пост написал только сейчас. Зато он получился более подробный, и надеюсь более понятный, потому что тема, о которой в книге идет речь, достаточно тяжелая и без привязки к конкретным задачам может показаться слишком абстрактной.
Про технику проектирования программных систем DDD (Domain-Driven Design) я слышал давно и разное, но не было хорошего понимания того, что это такое. Поэтому захотелось почитать какую-нибудь книжку, где были бы последовательно описаны основные идеи этого подхода, чтобы можно было бы понять, имеет ли смысл такой подход использовать в своих задачах.
Domain-Driven Design (DDD), или как у нас принято переводить, предметно-ориентированное проектирование — это достаточно проработанная и замкнутая методология, которая описывает принципы построения архитектуры сложных корпоративных программных систем. Этот подход включает в себя два уровня проектирования: стратегическое проектирование, когда нужно строить систему для взаимодействия с заказчиком с одной стороны и множеством команд разработчиков с другой, а также тактическое проектирование, которое связано непосредственно с проектированием отдельных сервисов, что ближе и понятнее для архитекторов и программистов. Такое проектирование особенно хорошо подходит при использовании микросервисной архитектуры, а точнее подсказывает, как лучше разбивать большую систему на отдельные микросервисы, где между ними должна проходить граница. В книге описаны оба уровня проектирования по методологии DDD, а также рассказывается о том, как эту методологию можно связать с другими паттернами проектирования.
Давайте теперь разберем содержание книги более подробно.
Первая часть книги называется «Стратегическое проектирование». Кажется, это сама скучная глава для разработчика-программиста, и одна из самых важных для руководителя разработки, который взаимодействует с заказчиком. В этой части речь идет об организационных моментах взаимодействия между заказчиком и разработчиками, а также между командами, участвующими в разработке сложной системы. Но сначала нужно пообщаться с заказчиком и разобраться, на чем строится его бизнес, на чем он собирается зарабатывать деньги.
В разрабатываемой системе необходимо выделить несколько поддоменов (subdomains), которые отличаются тем, насколько они влияют на бизнес компании, являются ли они ноу-хау или все другие компании, работающие в этой области, скорее всего имеют подобные сервисы. Поддомены бывают нескольких видов: основные поддомены (core subdomains) включают в себя все то, что составляет главную особенность компании, то, чем они отличаются от конкурентов; универсальные поддомены (generic subdomains) объединяют все то, что не дает конкурентных преимуществ компании, но все подобные компании вынуждены это реализовывать (например, сервис авторизации и аутентификации пользователей); во вспомогательные поддомены (supporting subdomains) входят небольшие сервисы, которые не являются чем-то уникальным, но они позволяют функционировать сервисам из основного поддомена.
Подходы к разработке сервисов в этих поддоменах разный. Например, софт для основного поддомена является, как правило, сложным, но при этом он критически важен для компании, и его стоит разрабатывать собственными силами, не отдавая разработку на аутсорс. Это те сервисы, работа над которыми идет постоянно, пока функционирует компания. Решения для универсальных поддоменов выгоднее купить или использовать решения с открытыми исходниками. Для вспомогательных поддоменов лучше избегать разработки собственными силами, но при этом часто не существует готовых решений для задач из этой области.
Если субдомены разделяют деятельность компании на крупные области, то уже внутри субдоменов можно выделить так называемые ограниченные контексты. В книге этой теме так или иначе посвящено несколько глав, и термин «ограниченный контекст» будет встречаться на протяжении всей книги. Ограниченные контексты образуются из идеи, что разработчик и заказчик (а точнее, специалист предметной области, для которой разрабатывается софт) должны говорить на едином языке, который нужно поддерживать в актуальном состоянии. Не должно быть такого, что специалист из бизнеса и разработчик называют одну и ту же сущность разными словами. Этот единый язык должен проникать в код, который пишут программисты, в названия классов и переменных. Нужно составить словарь для единого языка. Но проблема в том, что у специалистов из разных областей одни и те же термины могут обозначать разные сущности или наоборот одну и ту же сущность у них принято называть разными словами. Поэтому нужно выделить такие области, внутри которых можно сформировать свой единый язык. Эти области и называются ограниченными контекстами. Это очень важное понятие, которое влияет на все дальнейшее проектирование сервисов, потому что каждый ограниченный контекст должен быть реализован как отдельный сервис или проект. По сути каждый ограниченный контекст является моделью какой-то предметной области, куда входит один или несколько сервисов.
Важно, чтобы каждый ограниченный контекст реализовывался только одной командой разработчиков, но при этом одна команда может заниматься разработкой нескольких ограниченных контекстов. А вот для того, чтобы собрать общую систему, ограниченные контексты должны как-то взаимодействовать между собой.
Интеграции ограниченных контекстов в общую систему в книге посвящена одна из глав, которая описывает паттерны взаимодействия между командами, разрабатывающие ограниченные контексты. Существует несколько паттернов взаимодействия. Коротко их перечислю.
Паттерн партнерство (partnership) подразумевает активное взаимодействие между командами и готовность одной команды подстраивать интерфейсы их сервисов под пожелания команды, разрабатывающей другой ограниченный контекст. Причем эти команды равноправны, поэтому хотелки могут прилетать как от одной команды, так и от другой.
Паттерн общее ядро (shared kernel) несколько противоречит идее, что ограниченные контексты должны быть полностью независимы. При использовании такого паттерна две команды работают над общим кодом. Это может быть полезно, если вы используете монорепозиторий, но при этом должна быть хороша налажена коммуникация между командами.
Команды могут быть организованы и не на равноправных условиях. Например, бывает, когда одна команда постоянно выдает новые версии сервисов из своего ограниченного контекста, а другая команда вынуждена подстраивать свой код под изменения первой команды. Такой паттерн называется конформист (conformist).
При этом нижестоящая команда для смягчения неожиданных изменений в сервисах первой команды может разработать промежуточный преобразующий слой между своим ограниченным контекстом и ограниченным контекстом вышестоящей команды, чтобы внутренние модели данных были меньше подвержены изменениям. Так работает паттерн предохранительный слой (anicorruption layer).
Может быть обратная ситуация, когда вышестоящая команда подстраивается под нижестоящую и предоставляет им редко меняющиеся интерфейсы. При этом именно вышестоящая команда разрабатывает прослойку с преобразующим данные интерфейсом. В этом случае у сервисов вышестоящего ограниченного контекста может быть несколько версий интерфейсов, которые поддерживаются какое-то время. Это паттерн сервис с открытым протоколом (open-host service).
В конце главы про интеграцию ограниченных контекстов говорится, что в процессе проектирования необходимо нарисовать карту контекстов, на которой должны быть отмечены все ограниченные контексты и типы (паттерны) их взаимодействия.
На этом часть книги про стратегическое проектирование заканчивается и начинается часть про тактическое проектирование, которое уже ближе к коду. Далее следуют несколько глав, которые посвящены паттернам, с помощью которых можно реализовать бизнес-логику.
Если у вас простая логика работы сервиса, которая по сути сводится к простейшим операциям с базой данных, то есть вам нужно всего лишь получать данные из базы данных и вносить изменения в отдельные таблицы без сложной логики взаимодействия с другими сервисами, то есть вам достаточно выполнять такие операции, которые обычно обозначают как CRUD (Create, Read, Update, Delete), то вам достаточно реализовать паттерн Транзакционный сценарий. В этом случае вы работаете с базой данных напрямую с помощью языка запросов или через тонкую программную прослойку. Однако такой паттерн скорее всего не подойдет для реализации бизнес-логики в основном поддомене, где алгоритмы взаимодействия должны быть более сложные.
При чуть более сложной логике работы может использоваться паттерн Активная запись, который описывает инкапсуляцию более сложных взаимодействий с базами данных в объекты. Внутри этих объектов может использоваться транзакционный сценарий, который я упомянул выше, но нужно следить за сохранностью данных, чтобы ошибки при выполнении транзакции не приводили данные в некорректное состояние. При использовании паттерна «активная запись» часто для доступа к данным используется объектно-реляционное отображение (object-relational mapping, ORM), когда разработчик не составляет запросы сам, а использует методы классов, описывающие модель данных. Такой паттерн тоже не подходит для сложной бизнес-логики, и некоторые его считают антипаттерном и называют «анемичная модель».
Для более сложных алгоритмов работы бизнес-логики используется паттерн модель предметной области, который на более низком уровне может реализовываться с помощью так называемых тактических паттернов, о которых в книге рассказывается достаточно подробно. К таким паттернам относятся Объект-значение — это объект, который можно идентифицировать по составляющим его значениям, то есть ему не нужно иметь явный идентификатор, а из базы данных или из другого хранилища такие объекты запрашиваются по его значениям. Например, таким образом может быть реализован объект для хранения цвета, когда он идентифицируется значениями RGB, и дополнительный идентификатор не требуется. Такие объекты помогают лучше привязаться к единому языку и более наглядно показывают назначение объекта. Например, вместо простого строкового представления номера телефона можно создать объект-значение PhoneNumber, который будет хранить требуемое строковое значение, но при этом языки программирования будут проверять используемые типы, а сам объект-значение может проверять адекватность данных, которые предлагается ему хранить.
Противоположностью объектов-значений является паттерн сущность. Сущностями является все, что имеет уникальный идентификатор, по которому осуществляется выборка объекта из базы данных. Многие последующие паттерны будут работать именно с сущностями.
Если требуется хранить сложные согласованные данные, то применяется паттерн Агрегат. Это очень важный паттерн, который используется в паттернах более высокого уровня. Агрегат является сущностью, то есть его состояние хранится в базе данных, но при этом он сам объединяет в себе набор более мелких сущностей. Главная задача агрегата — обеспечивать согласованность данных при реализации сложной бизнес-логики, когда изменяются состояния сразу нескольких объектов. Для этого состояние агрегата может изменяться только с помощью команд, предоставляемых открытым интерфейсом агрегата. Важно правильно выделить границу агрегата, которая отделяет логику изменения состояния, за которую он отвечает, от «внешнего мира». Изменять непосредственно состояние агрегата внешним по отношению к нему средствами нельзя.
Целая глава посвящена паттерну Событие как источник данных, суть которого состоит в том, что в базе данных хранится не конечное состояние какого-то крупного объекта (агрегата), а события, которые приводили к созданию и изменению этого агрегата, а чтобы получить конечное состояние объекта, эти события нужно последовательно повторно выполнить локально. Это позволяет заодно иметь полную статистику по изменению объектов, что позволяет проводить более сложный анализ данных, что может быть полезно аналитикам.
Далее в книге рассматриваются более высокоуровневые паттерны, связанные со взаимодействием разных компонентов системы. Рассматриваются несколько архитектурных паттернов, включая слоеную архитектуру и принцип инверсии зависимостей, который реализован через порты и адаптеры. Еще один интересный паттерн, который здесь рассматривается — это разделение ответственности команд и запросов или сокращение по англоязычному названию — CQRS. Этот паттерн предлагает хранить одни и те же данные в разных моделях — одни модели будут использоваться в бизнес-логике и являться источником истины, а другие модели будут обновляться для выполнения каких-то статистических и аналитических запросов, причем таких разных моделей может быть несколько под разные задачи. Достаточно подробно описываются возможные способы синхронизации таких моделей.
Еще из высокоуровневых паттернов, которые описаны в книге, можно перечислить паттерны сага и диспетчер процессов, которые предназначены для сложного и долговременного взаимодействия между сервисами в системе.
На этом описание паттернов в книге заканчивается и следующая часть книги посвящена способам архитектурного проектирования систем. Основная тема нескольких следующих глав — как постепенно внедрять предметно-ориентированное проектирование в уже существующий проект. Сюда относится как рефакторинг кода, так и организационные моменты, связанные со взаимодействие команд, использованием единого языка, выявление и выделение поддоменов предметной области.
Одна из глав посвящена методологии EventStorming — разновидности мозгового штурма, который позволяет выделить все события, команды, акторов и некоторые другие моменты, связанные в работой системы и бизнес-процессов в целом. Все это делается с помощью разноцветных стикеров, которые расклеивают либо на большой доске, либо на стене, на которой закреплен большой белый лист бумаги. В процессе EventStorming стикеры перемещаются и упорядочиваются по определенным правилам, и в результате этих действий расположение стикеров должно подсказать границы будущих агрегатов и ограниченных контекстов. В этом мероприятии должны участвовать основные специалисты, понимающие бизнес-процессы. Польза от EventStorming заключается не только (и не столько) в том, что выстраивается модель, которая включает в себя события предметной области, команды, агрегаты и ограниченные контексты. В процессе EventStorming происходит обмен знаниями между заинтересованными сторонами, обнаружение конфликтов моделей и формулирование единого языка.
Последняя часть книги посвящена вопросам взаимоотношений предметно-ориентированного проектирования и других известных паттернов проектирования. В этих главах рассказывается как постепенно внедрять использование DDD в проект, который не использовал какую-либо другую методологию разработки архитектуры, и в результате его развитие замедлилось из-за того, что добавлять новые возможности становится все труднее, не разломав при этом половину системы. Небольшой раздел посвящен тому, как при этом убедить руководство, что внедрение DDD принесет пользу.
Часто проблемы с развитием проекта возникают, когда проект представляет собой громадный неповоротливый монолит, но и микросервисы сами, если их проектировать неправильно, могут не только не улучшить ситуацию, а сделать проект еще более запутанным из-за сложных взаимодействий между микросервисами. Для описания такой ситуации в книге используется термин «распределенный большой ком грязи». Одна из глав посвящена тому, на что обращать внимание при разбиении проекта на микросервисы, как выделить границы, в пределах которых работает каждый микросервис, как сделать, чтобы микросервис имел как можно более компактный API-интерфейс, но при этом выполнял как можно больше полезных действий. И да, не всегда монолит — это плохо.
Отдельная глава посвящена событийно-ориентированной архитектуре. Такая архитектура логично вытекает из микросервисной архитектуры, когда хочется, чтобы микросервисы обменивались данными асинхронно. И в этом случае возникает опасность превратить проект в «распределенный большой ком грязи», потому что теперь, ко всему прочему, не гарантируется порядок прихода сообщений от микросервиса к микросервису, да и вообще сообщения могут задержаться или потеряться из-за проблем с шиной доставки сообщений или из-за других сетевых проблем. В этой главе описываются паттерны для разработки сообщений, которыми обмениваются микросервисы. Такие сообщения делятся на события, команды и сообщения. Но кроме этого важно продумывать последовательность обработки сообщений взаимодействующими микросервисами.
И последняя глава коротко посвящена паттерну сеть данных. Этот высокоуровневый архитектурный паттерн позволяет собирать и накапливать данные, которые нужны не для бизнес-логики приложения, а для обработки их аналитиками. Паттерн «сеть данных» противопоставляется паттернам «хранилище данных» (data warehouse) и «озеро данных» (data lake), которое со временем может превратиться в «болото данных». Основная идея сети данных заключается в том, что за данные для аналитиков отвечают та же команда разработчиков, которая работает над сервисом, на основе работы которого эти данные агрегируются. Благодаря этому разработчикам приходится поддерживать обратную совместимость при изменении внутренних моделей данных или писать преобразования к требуемым для аналитиков формату данных. Но при этом нет единого хранилища для данных всех видов, аналитики используют разные источники для получения данных по разным ограниченным контекстам, о которых говорилось в самом начале книги.
Таким образом мы очень коротко и поверхностно прошлись, то тому, что написано в этой небольшой (чуть более 300 страниц) книге. В целом книга полезная, но польза от нее будет, если вы работаете над большим корпоративным проектом или проектом с большим количеством микросервисов, за которые отвечают разные команды. Хорошо, если есть опыт работы в таких проектах, иначе книга может показаться слишком теоретической, и описываемое в ней трудно связать с повседневной практикой. Код в книге встречается, но он носит больше иллюстративный характер, чтобы показать идеи инкапсуляции данных или взаимодействия между компонентами. В основном эта книга про крупные архитектурные решения, когда речь идет о разработке сервисов, взаимодействующих между собой по сети.
В целом книга полезная, воды в ней практически нет, все написано по делу, из-за этого ее надо читать очень внимательно. Но тема сама по себе достаточно абстрактная, поэтому читать может быть местами тяжеловато. Мое любопытство по поводу того, что такое предметно-ориентированное проектирование, полностью удовлетворила.
PS. Вы можете подписаться на новости сайта через RSS, Группу Вконтакте или Канал в Telegram.




Leave a comment