Domain-Driven Design: чистая архитектура снизу доверху

Когда мидл-разработчик дорастает до сеньора, его, обычно мучает вопрос:»как правильно писать приложение?» Понятно, что когда он был джуном, ему давали совсем атомарные задачи и он развлекался покрытием тестов или написанием контроллеров. Переход в мидлы знаменуется назначением разработчику более абстрактных задач вроде реализации сервисов, репозиторной части или интеграции с внешними сервисами посредством клиентов. Но в какой-то момент мидл начинает задавать самому себе вопросы:»как найти единственно правильный способ написать приложение с нуля

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

Да, мы уже знаем самые популярные практики: KISS, DRY, YAGNI, SOLID, что там ещё… Мы умеем их применять. Но нас не покидает чувство, что все эти практики объединяет общая научная основа. Знаете, это как с Менделеевым, который на основе закономерностей практически по наитию составил периодическую систему, а потом открыли электроны и всё встало на свои места.

У меня для вас хорошие новости: научная основа есть. Это предметно-ориентированное проектирование.

Но есть и плохая новость: тема настолько новая и непростая в изучении, что какая-никакая популярность к ней пришла только лет 5 назад, и до сих пор совсем немного разработчиков достаточно хорошо в ней разбирается.

Но есть ещё одна хорошая новость: в статье я постараюсь дать максимально понятный ответ, что же такое предметно-ориентированное проектирование.

И начнём мы, конечно, с антипримера: плохой архитектуры.

Плохая архитектура

Итак, что же такое плохая архитектура? Чем она плоха?

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

Три всадника апокалипсиса архитектуры продукта.

Три всадника апокалипсиса архитектуры продукта.

И первый критерий — это хрупкость.

Хрупкость — это состояние системы, при котором один компонент отвечает за множество реализаций.

Изменив такой компонент, вы рискуете изменить логику приложения ещё в нескольких местах.

Возьмём пример.

В нашем приложении есть функция, которая умеет считать сверхурочное время сотрудника. Она настолько полезна, что ею пользуется не только отдел кадров (для учёта переработки сотрудников во внутренних отчётах), но и бухгалтерия (чтобы начислять зарплату).

В компании решили сэкономить.

В компании решили сэкономить.

И отдел кадров, и бухгалтерия вызывают одну и ту же функцию.

Но вот вышел новый нормативный акт, который обязывает отдел кадров иначе учитывать переработки сотрудников: по повышенному коэффициенту. Разработчик исправляет коэффициент и отправляет изменения в прод. Отдел кадров удовлетворён.

Наступает 5 число, отдел кадров засыпает и просыпается бухгалтерия. Для бухгалтерии нормативные акты не поменялись, но она теперь рассчитывает переработки по повышенному коэффициенту. И работники получают больше денег за переработки.

Хорошо всем, кроме бухгалтерии. Она требует вернуть всё как было, но как это сделать, когда в единственной функции учтены новые требования отдела кадров?

Это и есть хрупкость. Мы сделали одно движение, а сломалось в нескольких местах.

Как быть?

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

Каждому — своё.

Каждому — своё.

Хорошо. Бывают ситуации, когда изменение одного компонента ломает систему во многих местах. Но бывают ли ситуации, когда мы вообще не можем ничего изменить?

Да, и это ещё один критерий плохой архитектуры. Жёсткость.

Жёсткость — это свойство системы, при котором любое изменение одного компонента неизбежно затрагивает другие.

На практике это означает, что жёсткие места очень сложно изменить. В случае хрупкости, любое изменение неконтролируемо ломает систему. Жёсткую же систему очень сложно изменить в принципе.

Возьмём пример из жизни.

Уважаемые коллеги написали приложение, в котором есть контроллер, сервис и репозиторий. Поскольку контроллеру нужно было DTO, то уважаемые коллеги решили брать его прямо из репозитория, дообогащать его разными полями из других приложений в сервисе и возвращать в контроллер.

Коллеги упростили как могли.

Коллеги упростили как могли.

Всё приложение было жёстко завязано на одно-единственное DTO. Ключевое слово: жёстко. Это DTO требовалось по контракту, и про него знал даже репозиторий. Других вариантов получить что-то кроме этого DTO, как мы понимаем, не было.

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

И не забывайте, что DTO — это компонент контракта с фронтендом. Что будет, если потребуется его изменить? Нам опять же, придётся переделывать всё вплоть до репозитория.

Тот самый случай, когда нарушение контракта с фронтэндом ломает репозиторий.

Тот самый случай, когда нарушение контракта с фронтэндом ломает репозиторий.

Как быть в этой ситуации? Противопоставлением жёсткости является гибкость.

Гибкость — это свойство системы, позволяющее изменять её с минимальными последствиями.

Что это значит в нашем случае? Какой должна быть система, чтобы мы могли создать новый endpoint с новым DTO за половину рабочего дня?

Очевидно, что если нам требуется изменить только контракт, то изменения должны коснуться только слоя контроллера. Поскольку именно он отвечает за контракт.

Как это сделать?

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

Сервис будет работать с бизнес-сущностью, репозиторий будет работать с репозиторной сущностью, а с DTO будет работать только контроллер? В таком случае, для того, чтобы сделать новый контроллер, нам потребуется просто написать его, написать новое DTO и преобразовать данные, полученные из сервиса, в это DTO.

4069726044298d7e3296da4408eb9bda.png

Да, я уже слышу возражения:, но ведь тогда нам придётся писать больше кода. Нам нужно будет спроектировать бизнес-сущности, репозиторные сущности, DTO контроллеров. Уйма времени. И я вам возражу в ответ:, но ведь тогда стоимость любой доработки будет одинаковой. Вы точно сможете оценить длительность выполнения задачи на планировании, не боясь провалиться в кроличью нору. И стоимость эта, кстати, будет невысокой. Поскольку дописать немного нового намного дешевле, чем переписать уже имеющийся кусок.

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

Эти ситуации возникают, когда приложение имеет признаки третьего критерия плохой архитектуры — неподвижности.

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

Представим ситуацию.

Например, вы любите Hibernate.

И вы написали приложение, в котором использовали Spring Data JPA вместе с Hibernate.

Hibernate, как мы знаем, позволяет привязать таблицы к сущностям прямо на их стороне. Вы взяли сущность и обогатили её аннотациями, которые чётко дают понять, что она работает с реляционной базой данных. Какие это могут быть аннотации? Table, Entity, Column, GeneratedValue, OneToMany и прочие. Реализация хранения данных в реляционной базе приколочена намертво.

Все любят Hibernate!

Все любят Hibernate!

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

А у нас, как мы помним, реляционный тип данных записан аж в бизнес-сущности. И чтобы добавить реализацию MongoDB, придётся переписать половину приложения. Причём проблема даже не в том, что нужно дописать реализацию, а в том, что для выкорчёвывания Hibernate из бизнес-сущности потребуется такое напряжение сил, что проще написать приложение заново. Потому что Hibernate, конечно, может мапить таблицы на что-то ещё, но большинство разработчиков вставляют табличные колонки прямо на бизнес-сущность.

ae4cb9c8ecac02636fb463e6e43a9abb.png

Решение, конечно, есть. И о нём, а точнее, о концепции проектирования, объединяющей все эти хорошие практики и встраивающей их в общую логику, мы и поговорим сегодня.

Это предметно-ориентированное проектирование, или Domain-Driven Design.

Domain-Driven Design

Предметно-ориентированное проектирование является более абстрактным воплощением объектно-ориентированного подхода, предоставляя более основательную научную базу.

33948ebd2bd38cbbc56fe9f408ef49fd.png

Что я имею в виду?

Объектно-ориентированное программирование распространяется на применение языка. предметно-ориентированное проектирование позволяет распространить объектный подход на все уровни разработки — от архитектуры приложений до межсервисной архитектуры.

Предметно-ориентированное проектирование включает в себя три понятия:

Разберём каждое из них.

Предметная область

Предметнаяобласть — это совокупность тесно связанных понятий, описывающих свойства и поведение в пределах интересов бизнеса.

Что это значит на реальном примере?

Мы пишем интернет-магазин. Какие понятия описывают его свойства и поведение?

Продавец, Покупатель, Товар, Корзина, Баланс, Цена. Совокупность этих понятий и будет являться предметной областью интернет-магазина.

Предположим, для интернет-магазина нужна служба доставки. Её предметная область будет совершенно другой.

Посылка, Автомобиль, Маршрут, Адрес.

Хотим реализовать собственную систему оплаты? Пожалуйста: Лицевой Счёт, Баланс, Сертификат, Чек, Покупатель.

8302948e56f7fc2ef656b8062fd4f896.png

Каждое из этих приложений обладает собственной предметной областью.

Хорошо, мы захотели написать такое приложение. Как нам организовать предметные области разных компонентов, чтобы они не мешали друг другу?

Здесь нам поможет Ограниченный Контекст.

Ограниченный контекст

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

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

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

Например, мы захотим открыть пиццерию. Тогда хорошей идеей будет переиспользовать службу доставки. И систему оплаты тоже.

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

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

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

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

Итак, мы разделили предметные области при помощи ограниченного контекста. Осталась маленькая деталь: договориться внутри команды (а под командой подразумеваются все, включая заказчика) о терминах предметной области. Такой коллективный язык называется Единым Языком.

Единый язык

Единый язык — это общий язык терминов и понятий, описывающих предметную область в рамках ограниченного контекста.

Основная часть Единого Языка приходит в проект от бизнеса. Если вам в процессе разработки приходится сталкиваться с огромным количеством понятных только вашей команде сокращений и аббревиатур, то поздравляю — это и есть Единый Язык.

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

Итак, три основных понятия, на которых строится предметно-ориентированное проектирование:

Для закрепления материала нам осталось научиться применять предметно-ориентированное проектирование на практике. Затем мы применим концепцию DDD в рамках межсервисной и внутрисервисной архитектуры.

Но прежде давайте разберём такое важное явление, как интерфейсы.

Интерфейсы

Что такое интерфейс?

Интерфейс — это место подключения двух независимых систем.

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

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

И это будет интерфейс.

Интерфейс должен иметь контракт, позволяющий другим сервисам взаимодействовать с нашим.

bbfbab952388524f88e637a7fb7fb54d.png

Интерфейсы есть везде, где есть жёстко инкапсулированная система.

Классы взаимодействуют друг с другом через интерфейсы. Сервисы взаимодействуют друг с другом через API, которые также являются интерфейсами. Устройства взаимодействуют друг с другом через интерфейсы, коими являются, например, USB-порт или Wi-Fi-протокол. Даже человек взаимодействует с системой через пользовательский интерфейс.

И в нашем примере тоже будут интерфейсы.

Матрёшка ограниченных контекстов

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

У нас уже есть некий сервис, коим является микросервисное приложение «Интернет-магазин».

У «Интернет-магазина» есть своя предметная область, свой ограниченный контекст и свой единый язык.

API такого интернет-магазина будет включать в себя такие же высокоабстрактные понятия: добавление в корзину, пополнение счёта, оплата, доставка, отзыв. Мы, как потребители, взаимодействуем с интернет-магазином через интерфейс — пользовательский интерфейс.

4092e85ea924edd17bbcd30e03f7fb11.png

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

Ещё уровнем ниже мы найдём более низкоуровневые компоненты, имеющие всё то же самое.

И так вплоть до бизнес-сущностей.

Стратегическое проектирование. Межсервисное взаимодействие на примере

Итак, у нас есть несколько микросервисов, которые являются частью одной системы.

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

Для того, чтобы считаться хорошей, архитектура должна соответствовать как минимум трём критериям:

  1. Функциональная целостность.

  2. Принцип «Вход — Процесс — Выход».

  3. Слабые связи между компонентами.

Как наши модули будут взаимодействовать между собой? Наиболее очевидным способом будет перекрёстный вызов микросервисов по мере надобности. Например, если Витрине будут нужны детали Доставки, она может обратиться напрямую в Доставку. А сервис Биллинга может обратиться к Витрине, чтобы получить общую стоимость товаров и выставить счёт.

4097de07df4fbb210d39e05336dc4c28.png

Тем не менее, перекрёстные вызовы между микросервисами нарушают сразу несколько требований к хорошей архитектуре.

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

Система становится жёсткой.

Ситуация усугублятся, когда разные компоненты пишут разные команды. Выход? Нужно инкапсулировать сервисы друг от друга и исключить связи между ними.

Вторая проблема: где хранить совместные сценарии?

Предположим, Биллингу нужно получить от Витрины сумму покупок, а от Доставки — стоимость доставки. Или, например, Доставке нужно получить список товаров в рамках своего сценария. При всей кажущейся целостности системы сценарии будут размазаны по различным компонентам, что затруднит понимание системы и её поддержку. Такие сценарии явно превосходят предметную область каждого отдельного сервиса и не вмещаются в ограниченный контекст каждого из них.

6e4eb9fb2d5075ad2911b88b6c10c306.png

Сервисы становятся неподвижными.

Хочется выделить сценарии в отдельный компонент, как нечто более высокоуровневое, чем предметные области каждого из специализированных компонентов. Таким сценариям незачем погружаться в предметную область каждого сервиса, достаточно будет пользоваться их внешним API.

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

0134dfd359b1f6d5e90cd5346edadfd1.png

Приложение становится хрупким.

Как насчёт Kafka?

А что, хорошая идея. Более того, Эрик Эванс прямо рекомендует асинхронный обмен как наиболее подходящий при предметно-ориентированном проектировании. Какие проблемы нам удастся решить при помощи брокера сообщений?

Совершенно точно удастся решить проблему инкапсуляции сервисов. Теперь они не будут делать перекрёстные запросы, а просто станут откидывать и слушать события. Наше приложение стало менее жёстким, и это уже хорошо. Но что в остальном?

96cac9afc0927b543cde79a45d228053.png

А в остальном у нас всё так же. По-прежнему нет места для общих сценариев (брокер же не может хранить их), и нам нужно поддерживать точки входа для внешних потребителей. Приложение осталось неподвижным и хрупким.

Избавимся от сценариев и внешнего API?

Если невозможно победить хаос, его нужно возглавить. Что, если нам попросту избавиться от сценариев и внешнего API, раз они нам так мешают? Не насовсем, конечно, а вынеся их за пределы микросервисов, на уровень выше?

Мы создадим сервис, который будет знать все микросервисы и зависеть от них. Который будет отвечать за сценарии, внешний API и коммуникации между микросервисами. И Kafka будет не нужна.

Диспетчер?

Выделим несколько архитектурных паттернов, которые помогут нам создать такой сервис:

  • Фасад — для организации внешнего API.

  • Оркестратор — для хранения сценариев и оркестрирования работы микросервисов.

  • Посредник — для обеспечения взаимосвязи между микросервисами, сохраняя при этом инкапсуляцию между ними.

Пробежимся по каждому из паттернов.

Фасад. Скрывает множество API разных микросервисов, предоставляя общий единый API. Отныне многочисленные клиенты будут знать только его и взаимодействовать только с ним. Соответственно, изменения API внутренних сервисов будут слабо влиять на внешний API, особенно в той части, которая касается внутренних сценариев. Иными словами, минус хрупкость.

aef4b54a215fb1824d3f7b6022caf680.png

Из недостатков: многие endpoint-ы будут проксировать запросы в нижестоящие сервисы.

Оркестратор. Позволяет поднять сценарии на уровень выше. Нижестоящие микросервисы ничего не знают про сценарии, их работа отныне сведена к атомарным задачам. Минус неподвижность.

b1dece7fbcf460165333afbf158bb5e2.png

Посредник. Исключает взаимодействие микросервисов друг с другом. Отныне сцепка между ними будет равна нулю, что позволит развивать их максимально гибко. Минус жёсткость.

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

Кстати, предлагаю назвать его Диспетчер.

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

1c1621dd6c22c417ffbbc3543d396833.png

Фуф, с межсервисной архитектурой мы разобрались. Но как быть с архитектурой внутрисервисной? Как обеспечить следование предметно-ориентированному проектированию при написании каждого микросервиса?

Спустимся на уровень ниже.

Тактическое проектирование. Внутрисервисная архитектура

Мы разобрались, как применять принципы предметно-ориентированного проектирования на межсервисном уровне. Давайте разберёмся, как применять эти принципы на уровне внутрисервисном.

Поможет нам в этом луковичная архитектура.

Луковичная архитектура

Луковичная архитектура очень наглядно описывает то, как предметная область ограничивается своим контекстом. Так же, как и в предметно-ориентированном проектировании, предметная область является центральным элементом структуры приложения. Только называется по-другому:  бизнес-сущности.

Бизнес-сущности

Бизнес-сущности ничего не умеют делать и ничего не знают. Они полностью беспомощны. Они просто существуют, и всё. Их единственная задача — наполнять своим существованием предметную область приложения.

fe92b0f08b59b0ee84d44f94717f5f6f.png

И именно они являются той самой предметной областью.

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

Анатомия бизнес-сущности

Высшей формой организации бизнес-сущности является Агрегат.

2dc554e3ac47ee82374e9ce46805764e.png

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

Например, в нашем микросервисе «Витрина» существует Агрегат «Товар». Этот Агрегат будет содержать в себе всё, что касается товара, например:

  • свойства Товара;

  • корзины, в которых добавлен Товар;

  • историю изменения стоимости Товара, и так далее.

Агрегат включает в себя компоненты трёх типов:

  • корень Агрегата;

  • другие бизнес-сущности;

  • объекты-значения.

Корень Агрегата — это ключевая сущность Агрегата. Она владеет всеми остальными его элементами. Имя Корня Агрегата является концептуальным именем Агрегата. Для нашего Агрегата «Товар» Корнем будет бизнес-сущность Good.

Объекты-значения — это объекты в пределах Агрегата, которые не являются самостоятельными сущностями, а содержат специфичные для Агрегата данные, не применимые и не имеющие смысла за его пределами. У Объекта-значения даже нет собственного идентификатора в системе, он инициализируется вместе со своим Агрегатом.

ea53021282c5a20c18896fcebd2a8859.png

Для нашего Агрегата «Товар» таким Объектом-значением может являться История изменения стоимости. Мы не можем применить стоимость товара в отрыве от Товара где-то ещё, поэтому эти данные имеют смысл только в привязке к Товару и могут быть внутренним классом.

Другие сущности тоже могут входить в Агрегат в качестве его частей. При этом они могут использоваться отдельно от него Агрегата и входить в состав других Агрегатов. Такие сущности имеют собственную идентичность и хранятся отдельно.

Казалось бы, что может быть проще бизнес-сущностей? Ан нет, выясняется, что и они чётко структурированы в пределах предметной области. Мы храним бизнес-сущности в пакете domain на верхнем уровне компоновки.

Идём дальше.

Сервисы

Как мы выяснили ранее, бизнес-сущности сами по себе ничего не умеют. Умения бизнес-сущностей сконцентрированы в сервисах.

Сервис — это класс, который реализует бизнес-логику.

Сервис состоит из двух обязательных компонентов:

  • интерфейса, предоставляющего контракт и декларирующего бизнес-логику;

  • N-ного количества реализаций

16a294a1277bc6108c663fbe3ff44e29.png

Работа сервисов основана на правилах.

Правило первое:  сервис работает только со своей бизнес-сущностью и ничем другим. Или, если точнее, с Агрегатом. Сервисы работают с другими Агрегатами только через их сервисы. Это важно. Сервис не может работать с другими Агрегатами напрямую, только со своим.

Могут ли сервисы работать с несколькими сущностями сразу? Могут. Но это не сервисы в привычном понимании. И эта тема настолько обширна, что тянет на отдельную статью. Разберём это в другой раз.

Правило второе: архитектура зависимостей сервисов должна быть подчинена жёсткой иерархии и повторять иерархию Агрегатов. Сервисы не должны быть циклично зависимы. Если сервис А зависит от сервиса Б, то Б не может зависеть от А.

И, наконец, правило третье. Сервисы взаимодействуют друг с другом только через интерфейсы.

Точки взаимодействия предметных областей

Хорошо. Мы разобрали концепцию предметно-ориентированного проектирования. Изучили предметные области, определили роль ограниченного контекста, познакомились с единым языком. У каждого нашего сервиса присутствуют все атрибуты DDD. Всё круто, мы красавчики.

Но есть один момент. Эти уникальные предметные области, жёстко инкапсулированные при помощи ограниченных контекстов, должны как-то взаимодействовать. И не где-то в абстракции, а в конкретном месте в коде вашего сервиса.

Где? Предположим, у нас есть бэкенд, фронтенд и база данных. Предметная область бекенда состоит из бизнесовых понятий. Предметная область фронтенда состоит из элементов UI. Предметная область базы данных состоит из колонок и таблиц, скриптов и хранимых процедур.

Бэкенд ничего не знает про UI фронтэнда и таблицы базы данных. Фронтенд ничего не знает про бизнесовые понятия и колонки с хранимыми процедурами. База данных ничего не знает про бизнес-сущности и UI. И всё это пересекается где-то в бэкенде.

Но и в бэкенде должно быть место, где эти предметные области соприкасаются. И это место:  Data Access Object.

Data Access Object

Data Access Object является третьим слоем «луковичной архитектуры». В отличие от сервисного слоя, DAO не подчинён предметной области приложения. Он подчинён предметной области внешнего интерфейса. И именно DAO является тем местом, где предметные области взаимодействуют между собой.

Data Access Object — архитектурный слой, взаимодействующий с внешним интерфейсом и подчинённый его предметной области. В слое DAO происходит стратегическое связывание предметных областей.

Как происходит связывание предметных областей на конкретном примере? У нашего приложения есть предметная область — бизнес-сущности (Агрегаты). Такая предметная область есть и у стороннего сервиса — это Data Transfer Object (DTO). Как мы уже выяснили, преобразование бизнес-сущности в DTO и обратно происходит в слое DAO. Такое преобразование происходит в специальном месте — маперах.

b7b502cf0fbcd4a7e60095ae3fbb5c89.png

Мапер — это специальный класс, в котором происходит преобразование объекта одной предметной области в экземпляр другой.

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

Входящим параметром функции мапера всегда является преобразуемый объект одной предметной области. Результатом работы функции мапера всегда является получаемый объект из другой предметной области.

ef68aa9caf212605fe813757d031593a.png

Таким образом, местом взаимодействия двух предметных областей является слой DAO. Местом преобразования предметных областей являются маперы в пределах своего компонента DAO.

Подведём итог

Что даёт понимание предметно-ориентированного проектирования? Зачем нам это? Ведь можно же просто писать код и не париться.

Усложнение информационных технологий неизбежно.

Повышение уровня абстракции информационных систем неизбежно.

Развитие теории программирования неизбежно.

Когда я учился в школе и писал программы не Бейсике на советском компьютере УК-НЦ, для этого достаточно было выучить команды языка. Большинство программ были утилитами, вроде калькулятора, а компьютер на столе офисного работника был диковинным показателем статуса.

Информационные технологии 30 лет назад.

Информационные технологии 30 лет назад.

Но ситуация меняется. И слишком быстро.

Информационные системы огромны и абстрактны. Они объединяются в виртуальные сети, дополняя друг друга. Они развивают сами себя. Они захватили наш мир.

В последние полвека в сфере информационных технологий произошёл Большой Взрыв.

Чтобы взрыв оставался управляемым, нам, разработчикам, нужно иметь основательную базу теории программирования. Теория программирования должна развиваться так же быстро, как системы, которые мы строим. Иначе, в один момент всё рухнет, как плохо спроектированный мост. И результаты будут катастрофическими.

Информационные технологии сегодня.

Информационные технологии сегодня.

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

© Habrahabr.ru