Pull to refresh

Поваренная книга разработчика: Domain Driven Design рецепты ( 2-я часть, структура и взаимодействие )

Reading time 12 min
Views 20K

ddd-header


Введение


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


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


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


DDD


Вспомним данное ранее определение:


Проектирование на основе предметной области (DDD, Domain-driven design) — это подход к разработке программного обеспечения для комплексного удовлетворения потребностей, путем сильной связи реализации с основными бизнес-моделями, находящимися в процессе постоянного развития.

Эталонной книгой которая описывает практики построения сложных систем является книга Domain Driven Dedign (Big Blue Book) Эрика Эванса. Если вы читали любую обзорную статью по этой теме, вы уже знаете об этом. К моменту применения DDD на практике вам придется ее прочесть. Это не самая легкая книга для восприятия:


The canonical source for DDD is Eric Evans's book. It isn't the easiest read in the software literature, but it's one of those books that amply repays a substantial investment.

Martin Fowler: 15 January 2014

Если пролистать содержание книги, она покажется вам не совсем структурированной. Но нам поможет карта.
DDD-map


На карте выделены те практики, которые мы сегодня рассмотрим.


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


  • Повысить производительность.
  • Писать понятный код.
  • Масштабирование на уровне разработки программного обеспечения.

Единый язык


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


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

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


Хорошим примером модели будет являться топографическая карта. Она является моделью местности. Карта не содержит лугов полей и рек, она только отражает месторасположение реальных объектов относительно друг друга.


Чтобы построить четкую и ясную для всех модель, нужно говорить на одном языке. Об этом нам говорит не только Эрик Эванс, но и здравый смысл. Если программисты будут использовать свои термины, а бизнес свой 'сленг', то первые просто не будут понимать что нужно делать. Бизнес же в этом случае не сможет осознать реальной стоимости разработки той или иной 'фичи'. Сколько раз вам приходилось слышать: "Да это всего лишь добавить кнопку"?


Ваша цель, как проектировщика системы, должна быть в том, чтобы добиться от всей команды максимального понимания друг друга. Как же этого добиться? Начните говорить. Если люди начинают общаться в любой тесной группе, у них появляется некий общепринятый набор терминов. В различных компаниях процесс введения общего языка, скорее всего, будет отличаться. Это может быть как волевым решением, так и демократической процедурой. Язык может быть обозначен явно, так и вводиться не явно, в этом случае на нем начинают просто говорить. Хорошим приемом введения общего языка станет общая документация.


Как вести документацию проекта


  1. Любое общение между бизнесом и разработкой должно улучшить вашу модель.
  2. После совещания зафиксируйте результат в виде документации (артефакт Scrum), и покажите эту документацию всем участникам процесса разработки.
  3. Используйте в документации единый язык.
  4. Самое важное: не тратьте время на документацию. Вам еще придется писать код, а документация будет переписываться многократно, тратить ресурсы — это дорого. Вместо того, чтобы долго возиться с приложением для рисования диаграмм UML, воспользуйтесь салфеткой, ручкой и фотоаппаратом на телефоне.
  5. Документация требует дисциплины, нельзя писать ее время от времени.
  6. Разделите документацию:
    • Комментарии в коде — описывайте непонятные моменты прямо в коде, оставляйте #ТODO: (убирайте, когда сливаете код в master). Выражайте свое мнение в комментариях, к примеру вам приходиться использовать тот или иной костыль при работе с legasy кодом.
    • Комментарии к проекту README.md в корневом каталоге вашего проекта должны содержать техническую информацию: как запустить проект, как прогнать тесты, и т.п. Так же неплохо завести карту, где у вас лежат все проекты, и на каких серверах они запущенны. Отдельно записать все принятые соглашения.
    • И самое важное — база знаний. Сборник документов, описывающих бизнес процессы, это та часть документов, которая доступна как вам, так и бизнесу.
  7. Основная ошибка тех, кто пишет документацию, избыточность. Не пытайтесь охватить все и вся, передавайте только общий смысл. Документация должна дополнять ваш проект, но никак не заменять его. Не записывайте все термины, только имеющие неоднозначное значение. Если определение занимает больше двух предложений, это плохое определение.

Пример документации:


# Система авторизации

Система авторизации отвечает за идентификацию конкретного пользователя.

# Сущности:

Пользователь характеризуется:
- Именем и фамилией
- email
- телефоном

## Процессы:

### Регистрация
В процессе регистрации мы создаем нового пользователя, просим его подтвердить его email и телефон, авторизуем пользователя на 1 день (за это время он обязан подтвердить email и телефон).

### Авторизация
В процессе авторизации мы выписываем пользователю аутентификационный ключ на месяц. Аутентифицироваться может только пользователь с верифицированным телефоном и и email адресом.

### Верификация email
На email пользователя приходит письмо со ссылкой. Открывая ссылку пользователь сообщает, что это его email. Ссылка действительна сутки.

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

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

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

Заметим, что в данном примере мы не указали явный словарь, тем не менее мы закрепили понятие Пользователь, Авторизация, Регистрация. Написание подобной документации не займет у эксперта больше 20 минут.


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


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

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

История Абердитов, Кристов Мартин Виланд

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


Представим себе, что мы выступили в роли проектировщика прогрессивного стартапа. Все мы любим остывшую пиццу, ругаться с курьерами и часами заполнять формы на сайте. Поэтому мы придумали замечательный стартап "Четыре черепахи и одна крыса":


  • Есть сайт, на котором регистрируются пиццерии и вывешивают свои актуальные блюда.
  • У данных пиццерий нет своей курьерской службы.
  • Есть заказчики, которые не могут дойти до пиццерии, но они готовы сделать заказ через сайт или мобильное приложение.
  • Киллер 'фича': курьерами выступают не специально нанятый персонал, а простые люди, зарегистрировавшиеся через мобильно приложение
  • Курьеры получают заказы, после их исполнения им приходит оплата за проделанную работу.
  • Ждать таких курьеров приходится дольше, но и доставка, а соответственно и пицца выходит дешевле.

Давайте вспомним документацию, которую мы описывали в предыдущей главе. Там в нашем едином словаре использовался термин регистрация. Но в данном проекте мы имеем их несколько:


  • Регистрация заказчика
  • Регистрация пиццерии
  • Регистрация курьера
  • Регистрация заказа

Унифицированный язык принадлежит к ограниченному контексту. Домен приведенной выше документации — 'Система авторизации'. Давайте попробуем выделить домены для нашего стартапа.


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


Домен (Domain) — это репрезентация реальной бизнес структуры, решающая определенную задачу.

Например: Система логистики, Система оплаты, Система авторизации, Система управления заказами.


Домен делится на сабдомены, которые описывают меньшие структуры, например: корзина заказов, система построения маршрутов.


Каждый домен имеет ограниченную зону ответственности — лимитированную функциональность.


Ограниченный контекст (Bounded context) — набор ограничений домена, помогающий сфокусироваться домену только на одной задаче для ее лучшего решения.

Мне нравится представлять этот термин в виде такой абстракции. Домен — это круг. Ограниченный контекст — это окружность.


Domain-context


Еще в терминологии DDD выделяют ядро.


Ядро (Core domain) — самый главный домен, наиболее емко характеризующий ваш бизнес.

Итак, домены проекта "Четыре черепахи и одна крыса":


Работа с пиццерией (Pizzarias)


Контекст: b2b все, что относится к работе с пиццериями


Сабдомены:


  • регистрация новых пиццерий
  • добавление ассортимента
  • обновления статуса наличия того или иного товара

Работа с клиентом (Clients)


Контекст: b2c, все что относится к работе с заказчиками пиццы


Сабдомены:


  • просмотр ассортимента
  • информационные материалы

Работа с курьерами (Delivery system)


Контекст: b2e, все что относится к работе с курьерами


Сабдомены:


  • регистрация курьера
  • выдача заданий
  • регистрация заявок на вывод заработанных курьером средств.

Система заказов (Order system)


Контекст: Ядро. Позволяет координировать все отдельные домены, обеспечивая полный цикл от получения заказа до доставки пиццы пользователю. Не является исполнителем, а исполняет роль дирижера.


Сабдомены:


  • принятие заказа
  • исполнение заказа
  • отслеживание статуса заказа

Система рассчетов (Billing)


Контекст: Содержит в себе все финансовые операции. Обеспечивает взаимодействие с процессинговым центром.


Сабдомены:


  • прием денег за заказы
  • выдача денег курьерам за выполненную работу

Система статистки (Statistics)


Контекст: Сбор и обработка (не выдача) аналитической информации.


Сабдомены:


  • статистики по средствам
  • статистики по заявкам

Система менеджмента (Managment panel)


Контекст: Выдача аналитической информации. Инструментарий управленческих решений.


  • аналитика на основе собранной статистики
  • премодерация выплат курьерам

На основе доменов давайте составим его карту.


Карта доменов (Context map) — графический инструмент, который позволяет описать связи между отдельными доменами.

Context-map


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


Самое важное в карте — мы видим связи межу доменами. Такая структура очень хорошо ложится на микросервисную архитектуру:


Главный принцип микросервисной архитектуры: слабая связанность и сильное сцепление.

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


Перевод данных терминов взят из официального русского перевода и, возможно, плохо отражает передаваемый смысл. В оригинале термины звучат как: Low coupling (связанность, зацепление, сцепление, сопряженность), high cohesion (связность, прочность).


Практика реализации доменного разделение


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


Основные принципы, которыми мы руководствовались:


  • Простота решения. Делать сложные вещи простыми, а не простые сложными.
  • Прагматизм. Всегда нужно смотреть по ситуации, и при отсутствии имеющегося решения вырабатывать новое. Старайтесь все типизировать, но избегайте догматизма.
  • Код != документация. Код это инструкции для машины, документация это инструкция для людей. Не нужно их путать и выдавать одно за другое.
  • SOLID

Как реализовать домены?


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


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

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


Как выделить связи между доменами?


Связи между доменами это всегда API. Это может быть RESTful json api, gRPC, AMPQ. Мы не будем в рамках данной статьи сравнивать один протокол с другим и выделять их преимущества и недостатки, у каждого из них есть своя область применения. Но все же, остановимся на общих рекомендациях:


Будте гибкими в выборе протокола и жесткими в однотипности его реализации.


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


С другой стороны, если вы реализуйте RESTful json, используйте один стандарт структуризации данных. Можете взять готовый например jsonapi или openapi. Если по каким-то причинам готовые решения вас не устраивают и вы чувствуете, что можете разработать свой стандарт — опишите и используйте его. Но применяйте его везде, не разводите "зоопарк" стандартов. Если вам нужно общаться с внешней системой, где про ваши стандарты ничего знают, напишите микросервис адаптер.


Adapter


Как реализовать сабдомены?


Как отдельные модули внутри микросервиса.


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

Как это все выглядит? Давайте рассмотрим на примере. Как мы помним, у нас есть домен Работа с курьерами (Delivery system) — у этого домена есть три сабдомена:


  • регистрация курьера (registration)
  • выдача заданий (tasks)
  • регистрация заявок на вывод заработанных курьером средств (withdrawal)
  • проверка того, что ваш микросервис работает, вспомогательное, техническое средство(healt_checker)

Представим это все в виде структуры папок:


$ tree --dirsfirst delivery_system

 delivery_system
 ├── app/
 │   ├── health_checker/
 │   │   └── endpoints.rb
 │   ├── registrations/
 │   │   ├── entities/
 │   │   ├── forms/
 │   │   ├── repositories/
 │   │   ├── interactor/
 │   │   ├── services/
 │   │   ├── validations/
 │   │   ├── endpoints.rb
 │   │   └── helpers.rb
 │   ├── tasks
 │   │   ├── entities/
 │   │   ├── queries/
 │   │   ├── repositories/
 │   │   ├── endpoints.rb
 │   │   └── helpers.rb
 │   └── withdrawals
 │       ├── entities/
 │       ├── forms/
 │       ├── repositories/
 │       ├── interactor/
 │       ├── services/
 │       ├── validations/
 │       ├── endpoints.rb
 │       └── helpers.rb
 ├── config/
 ├── db/
 ├── docs/
 ├── lib/
 │   ├── schemas/
 │   └── values/
 ├── public
 ├── specs
 ├── config.ru
 ├── Gemfile
 ├── Gemfile.lock
 ├── Rakefile
 └── README.md

Каждая папка в директории apps/ реализует тот или иной сабдомен, внутри каждого домена есть различные паттерны: entities, forms, services и др. Мы рассмотрим каждый из применяемых паттернов подробно в одной из будущих статей.


Каждый такой паттерн реализован в соответствующем пространстве имен (Namespace). Например форма создания завяки на выплату курьеру:


module Withdrawal # Имя сабдомена
  module Forms #Реализуемый паттерн
    class Create

    end
  end
end

Как реализовать связи между сабдомены?


Давайте рассмотрим конкретный пример. У нас есть учетная запись курьера: Registrations::Entities::Account. Она относится к сабдомену Registrations — так как мы рассматриваем данный домен не как процесс регистрации, а, скорее, как учетный стол и регистрационную книгу, о чем указанно в нашей документации для бизнеса.


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


  • Создание учетной записи (Registration)
  • Создание заявки на вывод заработанных курьером средств (Wihtdrawal)

Как мы видим эти два процесса относятся к разным сабдоменам — Registration и Wihtdrawal.


module Registrations
  module Serivices 
    class CreateAccount
      def call
        account = Entities::Account.new
      end
    end
  end
end

module Withdrwals
  module Serivices 
    class CreateOrder
      def call
        account = Registrations::Entities::Account.new
      end
    end
  end
end

В первом обращение ообращение к классу будет реализованно через вызов Entities::Account. А во втором случае через явный вызов Registrations::Entities::Account. Т.е. если мы явно указываем сабдомен, значит класс из другого сабдомена и так мы четко обозначаем связь.


Если класс не относится явно ни к одному из сабдоменов, есть смысл выносить его в папку lib/. Как правило, это классы, реализующие паттерн 'ValueObject'. Мы рассмотри этот паттерн подробнее в одной из следующих статей.


Реализация через модель.


Процитирую Эрика Эванса:


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

Давайте вспомним пример хорошей модели, который я уже приводил в начале этой статьи — топографическая карта. У нас стоит цель — иметь возможность быстро найти расстояние между двумя населенными пунктами. Мы могли бы воспользоваться справочником-таблицей с указанием двух точек между городами. А можем воспользоваться картой. И там и там мы получим один и тот же результат примерно за одно и то же время. Но карта компактнее, она точнее отображает предметную область, она универсальнее. Карта как модель невероятно выразительна. И если рассматривать ее в рамках данной задачи, то померить расстояние удобнее по карте, чем на самой территории, которую она отражает. Модель, которая отражает предметную область, может превосходить ее в некоторых свойствах. Это действительно потрясает воображение.


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




Источники вдохновения

Tags:
Hubs:
+9
Comments 4
Comments Comments 4

Articles