Главная » Хабрахабр » Поваренная книга разработчика: DDD-рецепты (3-я часть, Архитектура приложения)

Поваренная книга разработчика: DDD-рецепты (3-я часть, Архитектура приложения)

В рамках предыдущих статей мы выделили область применения подхода и рассмотрели основные методологические принципы Domain Driven Design.

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

WM

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

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

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

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

Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software

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

Похожие мысли возникали в голове у многих разработчиков и проектировщиков сложных систем.

В 2011 году вышла статья Роберта Мартина — Screaming Architecture, которая говорит о том, что ваш код не просто должен описывать предметную область, а орать о ней, желательно матом.

When you look at the top level directory structure, and the source files in the highest level package; do they scream: Health Care System, or Accounting System, or Inventory Management System? So what does the architecture of your application scream? Or do they scream: Rails, or Spring/Hibernate, or ASP?

Martin, 30 September 2011 Robert C.

Структура фреймворка не должна ограничивать вашу архитектуру. Роберт рассказывает, что код вашего приложения должен отображать деятельность приложения, вместо того чтобы подстраиваться под правила фреймворка. Ограничительные рамки являются инструментом. Приложение, в свою очередь, не должно привязываться к БД или http протоколу, это всего лишь механизмы хранения и доставки. Тесты вашего приложения — это тесты логики его работы, а не тестирование http протокола. Не следует становиться адептом фреймоворка.

В ней автор рассказывает, как добиться того, чтобы код кричал. Через год выходит следующая статья Роберта Мартина — The Clean Architecture. Изучив несколько архитектур, он выделяет основные принципы:

  1. Независимость от рамок. Архитектура не зависит от какой-то существующей библиотеки. Это позволяет использовать фреймворки как инструменты, а не ограничения, связывающие ваши руки.
  2. Тестируемость. Бизнес-правила могут быть протестированы без пользовательского интерфейса, базы данных, веб-сервера или любого другого технического средства.
  3. Независимость от пользовательского интерфейса. Пользовательский интерфейс может легко меняться, не изменяя остальную часть системы. Например, веб-интерфейс можно заменить консольным интерфейсом, не изменяя бизнес-логику.
  4. Независимость от базы данных. Вы можете обменять Oracle или SQL Server на Mongo, BigTable, CouchDB или что-то еще. Логика вашего приложения не должна быть привязана к базе данных.
  5. Независимость от воздействия внешней среды. На самом деле ваши бизнес-правила просто ничего не знают о внешнем мире.

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

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

Результатом любого процесса являлся документ, который в конечном счете описывал тот или иной бизнес-объект. До появления компьютеров и языков программирования при построении и управлении системами со сложной бизнес-логикой использовался бумажный документооборот. В итоге делопроизводство сводилось к трем простым Действиям:

  1. Создание документа
  2. Обработка документа
  3. Работа с архивом документов
  4. Представление документа

Документ — фиксация информации о хозяйственной деятельности относительного того или иного реального бизнес-объекта.

В данный момент бумажные документы вытесняются электронными. Прошу заметить, что документ сам по себе не является реальным бизнес-объектом, а только его Моделью. Но вы можете представить, что сейчас вся ваша система, это система электронного документооборота, которая выполняет четыре простых Действия. Документом может быть запись в таблице, картинка, файл, отправленное письмо или любой другой фрагмент информации.
Я бы не хотел в дальнейшем использовать слово документ, так как оно будет вносить скорее путаницу, мы будем использовать понятие Сущность (Entity) из DDD терминологии.

  1. Collecting
  2. Processing
  3. Storage
  4. Representation

Действие (Action) — структурная единица деятельности бизнес-модели; относительно завершенный отдельный акт осознаваемой цели, произвольность и преднамеренность индивидуальной активности бизнес-объекта, различаемая конечным потребителем.

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

Режим (Conduction)- набор Действий в определенном порядке, имеющий законченный смысл, несущий пользу конечному потребителю.

Точнее "Timing mechanism for conducting a selected one of a plurality of sequences of operation", на который был получен патент US2870278A. Для подобных Режимов работы был придуман селективный кондуктор или Вариатор (Selector). Архитектурная "крутилка" приведена в начале статьи. Мы знаем это устройство как "крутилка" стиральной машины.

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

Если вы выбрали стирку, то ваша машина все равно прополощет белье, а затем его отожмет. Запуская стиральную машину, вы можете выбрать режим: стирка, полоскание или отжим. Отжим — финальное Действие в процессе стирки оно является самым “простым”. С полоскание в комплекте вы обязательно получите отжим. В нашей архитектуре самое простое ДейтствиеПредставление, с него и начнем.

Представление (Representation)

Мы даже можем выдать просто Code response — 200: Если говорить о чистом представлении без обращения к базе данных или внешнему источнику, то мы выдаем какую-то статическую информацию: html-страницу, файл, справочник лежащий в виде json'a.

Напишем простейший "Health checker"

module Health class Endpoints < Sinatra::Base get '/check' do; end end end

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

Representation

Лирическое отступление

Не нарушает ли это принципа единственной ответственности? Я прошу заметить, что во фреймворке Sinatra класс Endpoints объединяет в себе как Router, так и Controller в одном классе. По факту, Endpoints это не класс, а слой, выраженный через класс, и зона его ответственности на более высоком уровне.

Они представлены не набором классов, а наименованием и реализацией функции. Ок, а как же Router и Controller? Один класс отвечает одной ответственности, но не пытайтесь выразить каждую ответственность через класс. А статический файл это вообще файл. Исходите из практичности, а не из догматизма.

Работа с системой хранения (Storage)

Зачем кому-то нужен ваш сервис, если в нужный момент мы не можем его использовать? Бизнес требователен к доступности вашего приложения. Для обеспечения целостности данных мы фиксируем изменение состояния бизнес-объекта после каждой обработки.

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

module Reception class Endpoints < Sinatra::Base # Show item get '/residents/:id', provides: :json do resident = Repository::Residents.find params[:id] status 200 serialize(resident) end end
end

Работа с системой хранения в виде графической схемы:

Storage

Данная модель не принадлежит ни одному из этих слоев. Как мы можем заметить общение между уровнем, отвечающим за Хранение, и уровнем, отвечающим за представление данных, реализовано через Responce model. По факту, это бизнес-объект и он находится на слое, отвечающим за бизнес-логику.

Обработка (Processing)

Слой Интерактора является ключевым в нашем приложении, именно в нем описывается вся бизнес-логика в виде отдельных Вариантов использования (Use Cases) и именно на нем идет изменение Сущностей. Если речь заходит о том, что объектная модель изменяется на основе своих свойств без внесения новых данных, то мы обращаемся к слою Интерактора напрямую.

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

module Reception class Endpoints < Sinatra::Base # Register resident arrival post '/residents/:uid/arrival', provides: :json do result = Interactors::Arrival.call(resident_id: params[:id]) check!(result) do status 201 serialize result.data end end # Register resident departure post '/residents/:uid/departure', provides: :json do result = Interactors::Departure.call(resident_id: params[:id]) check!(result) do status 201 serialize result.data end end end end

Почему не сделать реализацию одним методом с параметром status? Давайте немного остановимся. Если к нам пришел постоялец, то мы должны проверить закончилась ли уборка, не поступало ли для него новых сообщений и т.п. Интеракторы Arrival и Departure в корне отличаются. Про сообщения, в свою очередь, мы даже не вспоминаем, поскольку если бы он был в гостинице, мы бы ему сразу позвонили. При его уходе мы, наоборот, должны инициировать уборку в случае необходимости. Именно всю эту логику бизнеса мы и прописываем на слое Интерактора.

Processing

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

Сбор данных (Collecting)

Эта форма проверяется. При первичной регистрации постояльца в гостинице он заполняет форму регистрации. Процесс возвращает данные — созданную бизнес-модель "Постояльца". Если данные верны, то происходит бизнес-процесс Регистрация. Эту модель мы представляем постояльцу в читаемой форме:

module Reception class Endpoints < Sinatra::Base # Register new resident post '/residents', provides: [:json] do form = Forms::Registration.new(params) complete! form do check! form.result do status 201 serialize form.result.data end end end end
end

Схематично это выглядит так:

Collecting

  • Вариативная система с точки зрения процессов делится на Действия.
  • Последовательность Действий определяется Режимом.
  • Режимы инкрементальны.
  • Более "сложный" Режим дополняет более "простой", на строго одно действие.
  • Каждое действие происходит в рамках одного Слоя.
  • Каждый слой представлен Классом.
  • Внутри слоя могут быть Классы-Слои и Классы-Ответственности.
  • Общение происходит только между Слоем и Внутрислойным Классом.
  • Модели-Представления являются исключениями.
  • Обработка ошибок должна происходить на уровне Класса-Cлоя.

Tree

Его применение требует от проектировщика большого опыта для четкого осознания решаемых задач. У данного подхода высокий порог вхождения. Но, не смотря на сложность структуры, реализация на уровне кода невероятно проста и выразительна. Сложность также представляет разнообразие выбора необходимого инструмента. В дальнейшем мы разберем каждый шаблон проектирования в отдельности, опишем как его создать, тестировать и обозначим область применения. Хотя и содержит в себе ряд условностей и доверенностей. А чтобы не запутаться в их многообразии, предлагается полная карта:

Карта в высоком разрешении

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


Оставить комментарий

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

*

x

Ещё Hi-Tech Интересное!

Зачем программисту стажировка на кухне — разговор с «Додо пиццей» про гембу, .NET и открытость

Бизнес компании оплетен сетью технологичных сервисов, о своей истории они написали книгу, стек технологий и архитектура системы расписаны прямо на сайте, в паре кликов от главной страницы. Про «Додо пиццу» уже известно много. Даже самые неприятные факапы они спокойно и ...

[Перевод] Профилирование кода с LLVM

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