Domain modeling made functional pdf

Domain modeling made functional pdf

Domain modeling made functional pdf

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

​​Objective-C Programming: The Big Nerd Ranch Guide (2013)
Авторы: Aaron Hillegass

Целевая аудитория: начинающие разработчики.

Если вы хотите писать приложения для конкретной ОС: например, iOS, Mac, то вам обязательно надо изучать разработку на языке программирования Objective-C. Это руководство рассматривает основы языка, примеры использования, базовые концепции и лучшие практики.

В книге рассматриваются следующие темы:
✔️ основ языка;
✔️ строки, числа, массивы и основные типы данных;
✔️ функции;
✔️ управляющие конструкции;
✔️ указатели и ссылки;
✔️ структуры и кучи и многое другое.

Преимущества:
небольшой объём;
многочисленные примеры, иллюстрации и таблицы;
удобное форматирование кода.

Недостатки:
некоторые примеры не работают.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

​​Understanding Linux Network Internals: Guided Tour to Networking on Linux (2009)
Автор: Christian Benvenuti

Целевая аудитория: начинающие разработчики.

Настоящее руководство объясняет основные концепции и сведения протоколов TCP/IP и основы работы ОС Linux. Также вы узнаете про основные трудности с пониманием работы Linux для сетевого администрирования у начинающих разработчиков и примеры решения различных задач.

В книге рассматриваются следующие темы:
✔️ основы Linux;
✔️ сетевые драйверы;
✔️ TCP/IP;
✔️ настройка веб-серверов и многое другое.

Преимущества:
многочисленные примеры;
актуальный материал и по сей день.

Недостатки:
большой объём.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

​​Успейте на всемирную распродажу «Киберпонедельник». Скидки до 40%!

​​Можно быть продвинутым разработчиком, но не уметь использовать Python для data анализа. А можно быть новичком в разработке, но за 2 месяца научиться анализировать данные как профи.
Курс «Python для анализа данных» от SkillFactory станет точкой входа в профессию веб-специалиста!
Python для анализа поможет генерировать бизнес-идеи за счет быстрой обработки больших массивов данных, проектирования системы сквозной аналитики, построения автоматически обновляемых отчетов.
Закажите план развития просто сейчас → https://goo.gl/2dfwiP

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

Доменное моделирование в функциональном стиле. Скотт Влашин

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

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

Что-то из книжки утвердило подходы к проектированию, которые я использую сам. Что-то стало совсем новым. Но обо всём по порядку.

Прежде, чем мы начнём

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

В любом случае, если перевод вам не заходит, то вы можете:

Ну а теперь, к конспекту.

О чём книга

Во введении автор пишет:

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

В книжке автор покажет, как моделировать домен, используя только типы и функции. Сам я этим пользуюсь уже давно. Из недавних примеров: я так переписал Тяжеловато, а ещё делал доклад о «Чистой архитектуре во фронтенде». В обоих случаях предметная область спроектирована на типах и функциях.

Книжку стоит прочесть, говорит автор, если:

Автор приводит примеры кода на языке F#. В конспекте я перевёл некоторые примеры на TypeScript, чтобы большему количеству читателей они были понятны. Перевести получилось не всё, потому что в F# есть фичи, которых нет в TypeScript. Поэтому я рекомендую прочесть книгу самостоятельно или хотя бы глянуть репозиторий с кодом из этой книги.

Сама книга разделена на 3 части:

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

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

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

Глава 1. Введение в DDD

Автор начинает главу с утверждения, что команде разработки нужно некое «общее понимание» предметной области (shared model), с которой связано приложение.

Как антипример он приводит команду, где анализ бизнес-требований, проектирование и разработку выполняют разные люди. Такое разделение опасно, потому что разработчики в конечном итоге могут не понимать, с чем именно они работают. Если эксперты отрасли, аналитики, архитекторы и разработчики используют разные понятия для одинаковых вещей, то смысл может сильно исказиться к концу «перевода».

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

Вместо этого автор предлагает использовать общее для всех понятие предметной области. Если понятия, которым выражаются аналитики и стейкхолдеры, выражены в коде в том же виде, то «потерь при переводе» будет меньше. Это даёт несколько преимуществ:

Чтобы состряпать такое общее понимание, автор приводит несколько рекомендаций:

Понимаем домен через бизнес-события

Первое правило — «Фокусироваться на событиях, а не структурах данных». Бизнес не просто работает с данными, он их изменяет. Мы можем думать о бизнес-процессах, как о серии изменений данных. Суть и ценность бизнеса заложены именно в этих преобразованиях, поэтому важно понять, что именно происходит с данными во время изменений.

Изменения данных случаются не сами, их что-то запускает. Эти триггеры автор называет доменными событиями (domain events). События — это стартовая точка всех процессов, которые мы собираемся моделировать в приложении. Автор предлагает записывать события в прошедшем времени — «Что-то случилось».

Чтобы определить доменные события, можно пользоваться событийным штурмом (event storming). В нём участвуют все, кто понимает, как работают разные части бизнеса. Во время штурма участники называют и записывают, что должно случиться, чтобы какой-то процесс начался. Далее записывают, что случается по ходу и после. Часто события будут складываться с цепочки, это нам пригодится в будущем.

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

— В нашем отделе мы обрабатываем заявки.
— Что запускает подобную работу в вашем отделе?
— Мы получаем заявку от клиентов по почте.
— Значит, мы можем записать событие «Заявка получена».
— Мы выполняем эти заказы, когда они подписаны.
— В какой момент это происходит?
— Когда мы получаем заказ от отдела приёма заказов.
— Можем назвать это событие «Заказ составлен».
…и т. д.

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

Некоторые события «тянут» за собой рабочие процессы (workflows) типа «Разместить заказ», «Отправить заказ» и т. д. Чем больше событий мы видим, тем в более крупные процессы они начнут складываться.

Такой штурм хорош тем, что:

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

Когда события собраны, участники думают, какие действия к ним привели. Эти действия автор называет командами (commands). В книге они записываются в виде повелительного глагола: «Сделай это». Как правило, результат команды — это событие:

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

В итоге о процессе мы думаем как о функции с входными и выходными данными.

Разделяем домен на поддомены

Обычно мы решаем большие задачи, разделяя их на задачи поменьше — с доменом так же. Мы можем выделить несколько частей «процесса обработки заказа»: приём, сборка, отправка и т. д. У бизнеса, как правило, для этих частей уже есть разные отделы. Такие части мы будем называть поддоменами.

Вообще, определение домена очень абстрактное — это область связанного знания. …Мы будем думать о нём, как о том, где «доменный эксперт» является собственно экспертом

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

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Несколько предметных областей могут пересекаться

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

Используем ограниченные контексты

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

Тут появляется различие между «проблемой» (problem space) и «моделью» (solution space). Модель содержит только то, что нам необходимо:

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

При преобразовании реального мира в модель детали могут потеряться, это нормально

В модели мы отображаем домены и поддомены как ограниченные контексты (bounded contexts) — части общей модели, каждая из которых моделирует один поддомен.

Где-то я ещё видел перевод bounded context как связанного контекста, но по ходу книги значение больше склонялось к ограниченному контексту.

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

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

Не всегда домены и контексты соотносятся 1 к 1. Бывает, что домен разбивают на несколько контекстов или несколько доменов моделируют через один контекст. Зависит от задачи. Но важно, что у каждого контекста есть лишь одна чёткая ответственность.

Выделить контексты непросто, несколько советов для этого:

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

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Карта контекстов в процессе обработки заказов

Некоторые домены более важны для бизнеса и, собственно, приносят деньги — это корневые (core) домены. Те, что помогают работе корневых, называются поддерживающими (supportive). Неуникальные для бизнеса, которые можно зааутсорсить — общие (generic).

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

Создаём общий язык

Общий или повсеместный язык (ubiquitous language) — то, что поможет команде оперировать одинаковыми словами, говоря об одинаковых понятиях.

При проектировании мы создаём и используем именно его. В таком языке мы описываем понятия теми терминами, которые используют доменные эксперты. Если кто-то говорит «Заказ» (Order), то именно так мы должны назвать понятие, о котором идёт речь. И наоборот, в дизайне не должно быть слов, которые не знакомы доменным экспертам: OrderFactory, OrderMapProcessor и прочее.

Глава 2. Изучение домена

В этой главе мы возьмём один конкретный рабочий процесс и детально его изучим:

Интервьюируем доменного эксперта

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

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

Также определите, что в рабочем процессе — входные данные. Например, в процессе «Приём заказа» (Place Order) входными данными будет «Форма заказа» (Order Form). Выходными данными всегда будут некоторые события, например, в процессе «Приёма заказа» таким событием может быть событие «Заказ принят» (Order Placed).

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

Если вы много работали с базами данных, то на этом этапе можете почувствовать желание начать проектировать таблички. Domain Drive Design (DDD) же предлагает принцип второстепенности хранилища (persistence ignorance).

Если проектировать под БД, то есть риск (умышленно или по неосторожности) упустить детали домена, чтобы впихнуть его в БД. Например, две с виду похожие, но разные по смыслу сущности в БД могут оказаться внутри одной таблицы и отличаться флагом. Позже с ними будет неудобно из-за этого работать.

Боремся с желанием проектировать классы

Если у вас много опыта в ООП, то вы тоже можете попасть в ловушку, если начнёте проектировать классы в голове. Например, для приложения из примера могло получиться нечто вроде:

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Диаграмма классов, которую можно (но не нужно) построить

Мораль здесь в том, что:

Нам следует внимательно собирать информацию о домене и не следует примешивать в него технические идеи и детали

Документируем домен

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

Например, процесс приёма заказа тогда будет выглядеть примерно так:

Данные же мы бы описали примерно так:

Или на английском:

Вместо И мы можем использовать ИЛИ, если какие-то части необязательные или взаимоисключающие.

Смысл такого формата в том, что он не страшен «непрограммистам». То есть описания процессов и данных можно показывать и обсуждать с доменными экспертами. Это позволит выявлять ошибки раньше.

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

Погружаемся в процесс приёма заказа

Когда мы разбираем бизнес-процессы детальнее, мы можем выявить детали, которых ранее не было заметно. В приложении из примера заказы (Orders) будут важнее, чем запросы цены (Quotes), потому что за заказы компания получает деньги, а запросы цен денег не приносят.

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

Или мы можем узнать, что не все товары продаются в штуках, некоторые могут продаваться в килограммах. А значит «Количество товара» (OrderQuantity) должно быть частью общего языка, чтобы под этим понятием все подразумевали одно и то же.

«Это зависит от чего-то…» Это те слова, после которых вам должно стать понятно — всё будет сложно

Отражаем сложность в модели

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

Мы узнали, что коды товаров и количества — не просто строки и числа, у них есть ограничения. Добавим их:

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

Продолжим уточнять ограничения. В количестве нам важно уточнить в каком промежутке могут быть значения. Это мы тоже узнаём у доменных экспертов:

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

Для тех, которым посчитали цену:

И результат процесса:

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

После того, как описаны данные, мы опишем и сам процесс:

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

И так далее для каждого шага этого процесса и других процессов.

Глава 3. Функциональная архитектура

В этой главе мы посмотрим на архитектуру программы с функционально ориентированным доменом. Мы будем считать, что она состоит из 4 «уровней»:

Ограниченные контексты как автономные программные компоненты

Каждый контекст в идеале — это автономная подсистема с чёткими границами. В начале проектирования, однако, нам не важно, как именно мы будем деплоить проект: микросервисно или как монолит. Главное — убедиться, что мы держим контексты расцепленными (decoupled).

Общение между контекстами

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

Данные, с которыми мы работаем внутри контекстов, будем называть доменными объектами (domain objects). Данные, которые мы передаём между контекстами хоть и могут быть похожими на доменные объекты, но ими не являются. Это будут специальные объекты передачи данных — data transfer objects, DTOs. Такие объекты, как правило, будут содержать ту же информацию, но структурированную так, чтобы объект было удобно сериализовать.

Границы каждого контекста будут играть роль «ворот» (gates). Всё, что приходит в контекст снаружи — это DTO, его надо проверить и валидировать. После валидации мы будем получать доменные объекты, с которыми можем работать, как с безопасными данными. Валидацией будет заниматься вход (input gate):

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Всё, что приходит снаружи — надо провалидировать, данные внутри считаем безопасными

Выходные ворота (output gate) будут проверять, чтобы в выходных данных случайно не оказалось лишней или чувствительной информации. Это улучшит безопасность и предотвратит зацепление между контекстами.

Контракты между контекстами

Какими бы расцепленными контексты ни были, общение между ними всё равно создаёт какое-то зацепление. Чтобы это не приносило боли, контекстам стоит выбрать формат сообщений, который они будут использовать при общении — выработать контракт.

Контракты и их выбор бывают разными:

При общении с внешними системами можно использовать анти-коррозионные слои (anti-corruption layers, ACL). В них данные преобразуются в такие, которые понятны и требуются нашей системе. Мы как бы подстраиваем внешний мир под себя, а не наоборот.

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

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Отношение между разными контекстами может отражать, как взаимодействуют отделы в компании

Процессы внутри контекстов

В функциональной архитектуре каждый из бизнес-процессов (workflows) — это функция, у которой вход — это команда, а выход — одно или несколько событий. Такие процессы всегда находятся внутри одного контекста и никогда не реализуют End-to-End процессов.

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

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

Структура кода внутри контекста

Центральной частью контекста будет домен, всё остальное будет расположено вокруг него. Зависимости будут направлены внутрь, то есть к домену. Этот подход известен как луковичная архитектура (onion architecture):

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Домен находится по центру, а ввод-вывод — по краям

Наша цель — работать с предсказуемыми функциями. В таких функциях не требуется смотреть на их внутренности, чтобы понять, что они делают. Для этого мы будем стараться как можно чаще использовать иммутабельные (неизменяемые) структуры данных, а все зависимости (dependencies) будем делать явными (explicit).

Весь ввод и вывод мы расположим по краям луковицы — в начале и конце процесса. Тогда корневая доменная логика будет отделена от хранилища, API и инфраструктуры. Так мы достигнем второстепенности хранилища (persistence ignorance).

В следующих частях

В этот раз мы поговорили о том, что такое домен и зачем он нужен.

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

Быстрорастворимое проектирование

Люди учатся архитектуре по старым книжкам, которые писались для Java. Книжки хорошие, но дают решение задач того времени инструментами того времени. Время поменялось, C# уже больше похож на лайтовую Scala, чем Java, а новых хороших книжек мало.

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

Эта статья является расшифровкой моего доклада с конференции DotNext 2018 Moscow. Кроме текста, под катом есть видеозапись и ссылка на слайды.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Коротко обо мне: я из Казани, работаю в компании «Хайтек Груп». Мы занимаемся разработкой ПО для бизнеса. С недавнего времени я преподаю в Казанском Федеральном Университете курс, который называется «Разработка корпоративного ПО». Время от времени я ещё пишу статьи на Хабр про инженерные практики, про разработку корпоративного ПО.

Как вы, наверное, могли догадаться, сегодня я буду говорить про разработку корпоративного ПО, а именно, как можно структурировать современные веб-приложения:

Критерии

Сформулируем критерии. Мне очень не нравится, когда разговоры про проектирование ведутся в стиле «моё кунг-фу сильнее твоего кунг-фу». У бизнеса есть, в принципе, один конкретный критерий, который называется «деньги». Все знают, что время — это деньги, поэтому две вот эти составляющие чаще всего самые важные.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Итак, критерии. В принципе бизнес чаще всего просит нас «как можно больше фич в единицу времени», но с одним нюансом — эти фичи должны работать. И первый этап, на котором это может сломаться, это код ревью. То есть, вроде бы, программист сказал: «Я за три часа сделаю». Три часа прошло, пришло в код ревью, а тимлид говорит: «Ой, нет, переделывай». Там ещё три — и сколько итераций код ревью прошло, на столько надо умножить три часа.

Следующий момент — это возвраты с этапа тестирования сдачи-приёмки. То же самое. Если фича не работает, значит, она не сделана, эти три часа растягиваются на неделю, две — ну там как обычно. Последний критерий это количество регрессии и багов, которые всё-таки, несмотря на тестирование и приёмку, прошли в продакшн. Это тоже очень плохо. С этим критерием есть одна проблема. Его сложно отслеживать, потому что связь между тем, что мы что-то такое запушивали в репозиторий, и тем, что потом через две недели что-то сломалось, бывает сложно отследить. Но, тем не менее, возможно.

Развитие архитектуры

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

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Поэтому получался вот такой архитектурный стиль. У нас это называется «лапшекод», за рубежом говорят «спагетти-код». Всё связано со всем: мы меняем что-то в точке А — в точке Б ломается, понять, что с чем связано, совершенно невозможно. Естественно, программисты довольно быстро сообразили, что так дело не пойдёт, и надо какую-то структуру сделать, и решили, что нам помогут какие-то слои. Вот если вы представите, что фарш — это код, а лазанья — это такие слои, вот вам иллюстрация слоёной архитектуры. Фарш остался фаршем, но теперь фарш из слоя № 1 не может просто так взять и пойти общаться с фаршем из слоя № 2. Мы придали коду какой-то форму: даже на картинке вы можете увидеть, что лазанья более оформлена.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

С классической слоёной архитектурой, наверное, все знакомы: есть UI, есть бизнес-логика и есть Data Access layer. Бывают ещё всякие сервисы, фасады и слои, названные по имени архитектора, который уволился из компании, их может быть неограниченно много.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

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

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

На самом деле нет. Вся разница в том, что где-то в это время сформулировали принципы SOLID, и выяснилось, что в классической луковой есть проблема с инверсией зависимостей, потому что абстрактный доменный код почему-то зависит от реализации, от Data Access, поэтому Data Access решили развернуть, и сделали так, чтобы Data Access зависел от домена.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Я вот здесь поупражнялся в рисовании и нарисовал луковую архитектуру, но не классически «колечками». У меня получилось нечто среднее между многоугольником и кружками. Я это сделал, чтобы просто показать, что, если вы встречали слова «луковая», «гексагональная» или «порты и адаптеры» — это всё одно и то же. Смысл в том, что домен в центре, его заворачивают в сервисы, они могут быть доменные или application-сервисы, кому как больше нравится. А внешний мир в виде UI, тестов и инфраструктуры, куда переехал DAL — они общаются с доменом через этот сервисный слой.

Простой пример. Обновление email

Давайте посмотрим, как в такой парадигме будет выглядеть простой use case — обновление email’а пользователя.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Нам нужно отправить запрос, провести валидацию, обновить в базе данных значение, отправить на новый email уведомление: «Всё в порядке, вы поменяли email, мы знаем, всё хорошо», и ответить браузеру «200» — всё окей.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Код может выглядеть примерно как-то так. Вот у нас есть стандартная ASP.NET MVC-валидация, есть ORM, чтобы прочитать и обновить данные, и есть какой-нибудь email-sender, который отправляет нотификацию. Вроде как всё хорошо, да? Один нюанс — в идеальном мире.

В реальном мире ситуация чуть-чуть отличается. Смысл в том, что надо добавить авторизацию, проверку ошибок, форматирование, логирование и профилирование. Это всё не имеет никакого отношения к нашему use case’у, но это всё должно быть. И вот тот маленький кусочек кода стал большим и страшным: с большой вложенностью, с большим количеством кода, с тем, что это тяжело читать, а главное, что инфраструктурного кода больше, чем доменного.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

«Где же сервисы?» — скажете вы. Я же записал всю логику в контроллеры. Конечно, это проблема, сейчас я добавлю сервисы, и всё будет хорошо.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

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

Стало лучше? Стало! А еще мы теперь этот метод можем повторно использовать в разных контроллерах. Результат налицо. Давайте посмотрим на реализацию этого метода.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

А вот здесь уже всё не так хорошо. Этот код никуда не делся. Всё то же самое мы просто перенесли в сервисы. Мы решили не решать проблему, а просто её замаскировать и перенести в другое место. Вот и всё.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Дополнительно к этому появляются некоторые другие вопросы. А валидацию мы должны делать в контроллере или здесь? Ну, вроде как, в контроллере. А если надо сходить в базу данных и посмотреть, что такой ID есть или что нет другого пользователя с таким email’ом? Хмм, ну тогда в сервисе. А вот обработка ошибок здесь? Эту обработку ошибок, наверное, здесь, а ту обработку ошибок, которая будет отвечать браузере, в контроллере. А метод SaveChanges, он в сервисе или надо перенести его в контроллер? Может быть и так, и так, потому что, если сервис вызывается один, логичнее вызвать в сервисе, а если у вас в контроллере три метода сервисов, которые надо вызвать, тогда надо вызывать его за пределами этих сервисов, чтобы транзакция была одна. Вот такие размышления наводят на мысль, что, может быть, слои не решают каких-то проблем.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Все эти люди говорят об одном и том же и предлагают строить приложения не на основе слоёв, а на основе вариантов использования, то есть тех требований, о которых бизнес нас просит. Соответственно, вариант использования в C# может быть определён с помощью интерфейса IHandler. У него есть входные значения, есть выходные значения и есть сам метод, который собственно выполняет этот сценарий использования.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

А внутри этого метода может быть как доменная модель, так и какая-нибудь денормализованная модель для чтения, может быть с помощью Dapper’а или с помощью Elastic Search’а, если надо что-то искать, а, возможно, у вас есть Legacy-система с хранимыми процедурами — нет проблем, а также сетевые запросы — ну и вообще всё что угодно, что вам там может потребоваться. Но если слоёв нет, как же быть?

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Для начала давайте избавляться от UserService. Уберём метод и создадим класс. И ещё уберём, и снова уберём. А потом возьмём и уберём класс.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Давайте подумаем, эти классы эквивалентны или нет? Класс GetUser возвращает данные и ничего не меняет на сервере. Это, например, про запрос «Дай мне ID пользователя». Классы UpdateEmail и BanUser возвращают результат операции и изменяют состояние. Например, когда мы говорим серверу: «Пожалуйста, измени состояние, надо вот что-то поменять».

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Посмотрим на протокол HTTP. Есть метод GET, который по спецификации протокола HTTP должен возвращать данные и не менять состояние сервера.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

И есть другие методы, которые могут менять состояние сервера и возвращать результат операции.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Парадигма CQRS как будто специально создана для протокола HTTP. Query — это GET-операции, а команды — это PUT, POST, DELETE — не надо ничего придумывать.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Доопределим наш Handler и определим дополнительные интерфейсы. IQueryHandler, который отличается только тем, что мы повесили constraint о том, что тип входных значений – это IQuery. IQuery — это маркерный интерфейс, в нём ничего нет, кроме вот этого дженерика. Дженерик нам нужен для того, чтобы поставить constraint в QueryHandler’е, и теперь, объявляя QueryHandler, мы не можем туда передать не Query, а передавая туда объект Query, мы знаем его возвращаемое значение. Это удобно, если у вас одни интерфейсы, чтобы потом не искать в коде их реализации, и опять же чтобы не напутать. Вы пишете IQueryHandler, пишете туда реализацию, и в TOut вы не можете подставить другой тип возвращаемого значения. Это просто не скомпилируется. Таким образом сразу видно, какие входные значения соответствуют каким входным данным.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

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

Реализация Handler

Handler’ы мы объявили, какая же у них реализация?

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Какая-то проблема есть, да? Кажется, что-то не помогло.

Декораторы спешат на помощь

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

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Тогда у нас всё будет выглядеть следующим образом: есть входное Dto, оно входит в первый декоратор, во второй, третий, дальше мы заходим в Handler и так же выходим из него, проходим через все декораторы и возвращаем обратно Dto в браузере. Объявляем абстрактный базовый класс для того, чтобы потом наследовать, в конструктор передаётся само тело Handler’а, и объявляем абстрактный метод Handle, в котором и будет навешиваться дополнительная логика декораторов.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

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

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Следующий этап — это Security. Так же объявляем декоратор, делаем метод CheckPermission, проверяем. Если вдруг что-то пошло не так, всё, не продолжаем. Теперь после того, как мы провели все проверки и уверены, что всё хорошо, мы можем выполнять нашу логику.

Одержимость примитивами

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

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

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

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Чтобы валидация работала, мы добавляем сюда какие-то атрибуты, которыми рассказываем, что это за валидация. Это поможет с точки зрения структуры данных, но не поможет с такой валидацией, как проверка значений в БД. Здесь просто EmailAddress, непонятно, как, где проверять, как эти атрибуты использовать для того, чтобы в базу сходить. Вместо атрибутов, можно перейти к специальным типам, тогда эта проблема решится.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Вместо примитива int объявим такой тип Id, у которого есть дженерик, что это вот определённая сущность с int’овым ключом. И в конструктор мы либо передаём эту сущность, либо передаём её Id, но при этом мы должны передать функцию, которая по Id может взять и вернуть, проверить, null там или не null.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Аналогично поступаем с Email. Преобразуем все Email’ы к нижней строке, чтобы у нас всё выглядело одинаково. Дальше берём Email-атрибут, объявляем его как статический для совместимости с валидацией ASP.NET и здесь его просто вызываем. То есть так тоже можно делать. Для того, чтобы инфраструктура ASP.NET всё это подхватила, придётся немножко изменить сериализацию и/или ModelBinding. Кода там не очень много, он сравнительно простой, поэтому я не буду на этом останавливаться.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

После этих изменений, вместо примитивных типов, у нас здесь появляются специализированные типы: Id и Email. И после того, как отработали вот эти ModelBinder и обновлённый десериализатор, мы точно знаем, что эти значения корректны и в том числе, что такие значения есть в БД. «Инварианты»

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Следующий момент, на котором я хотел бы остановиться, это состояние инвариантов в классе, потому что довольно часто используется анемичная модель, в которой есть просто класс, много геттеров-сеттеров, совершенно непонятно, как они должны работать вместе. Мы работаем со сложной бизнес-логикой, поэтому нам важно, чтобы код был самодокументируемым. Вместо этого лучше объявить настоящий конструктор вместе с пустым для ORM, его можно объявлять protected, чтобы программисты в своём прикладном коде не смогли его вызвать, а ORM смогла. Здесь мы передаём уже не примитивный тип, а тип Email, он уже точно корректный, если это null, мы всё ещё выбрасываем Exception. Можно использовать какие-нибудь Fody, PostSharp, но скоро выходит C# 8. Соответственно, там будет Non-nullable reference type, и лучше дождаться его поддержки в языке. Следующий момент, если мы хотим поменять имя и фамилию, скорее всего мы хотим их менять вместе, поэтому должен быть соответствующий публичный метод, который меняет их вместе.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

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

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

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

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

На самом деле не совсем. Дело в том, что Dto в общем-то не совсем объекты. Это такой словарик, в который мы засовываем десериализованные данные. То есть они притворяются объектами, конечно, но у них есть всего одна ответственность — это быть сериализованными и десериализованными. Если мы попытаемся бороться с этой структурой, начнём объявлять какие-то ModelBinder’ы с конструкторами, что-то такое делать, это невероятно утомительно, и, главное, это будет ломаться с новыми выходами новых фреймворков. Всё это хорошо описал Марк Симон в статье «На границах программы не объектно-ориентированы», если интересно — лучше прочитайте его пост, там всё это подробно описано.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

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

Handler

После того, как вот эти все изменения внесены, как у нас будет выглядеть Hander?

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Я здесь написал две строчки для того, чтобы удобнее было читать, а вообще можно записать в одну. Данные точно корректны, потому что нас есть система типов, есть валидация, то есть железобетонно корректные данные, проверять их повторно не нужно. Такой пользователь тоже есть, другого пользователя с таким занятым email’ом нету, всё можно делать. Однако ещё нет вызова метода SaveChanges, нет нотификации и нет логов и профайлеров, да? Двигаемся дальше.

Events

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Наверное, в первый раз популяризовал эту концепцию Уди Дахан в его посте «Domain Events – Salvation». Там он предлагает просто объявить статический класс с методом Raise и выкидывать такие события. Чуть позже позже Джимми Богард предложил лучшую реализацию, она так и называется «A better domain events pattern».

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Я буду показывать сериализацию Богарда с одним небольшим изменением, но важным. Вместо того, чтобы выбрасывать события, мы можем объявить какой-то список, и в тех местах, где должна происходить какая-то реакция, прямо внутри сущности сохранять эти события. В данном случае вот этот геттер email — это также класс User, и этот класс, это свойство не притворяется свойством с автогеттерами и сеттерами, а действительно что-то добавляет к этому. То есть это настоящая инкапсуляция, а не профанация. Когда меняем, мы проверяем, что email другой, и выбрасываем событие. Это событие пока никуда не попадает, оно у нас есть только во внутреннем списке сущностей.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

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

Реализация этого диспетчера — это тема отдельного разговора, там есть некоторые сложности с multiple dispatch в C#, но это всё тоже делается. С таким подходом есть ещё одно неочевидное преимущество. Теперь, если у нас есть два разработчика, один может писать код, который изменяет вот этот самый email, а другой может делать модуль нотификаций. Они абсолютно не связаны друг с другом, они пишут разный код, они связаны только на уровне этого доменного события одного класса Dto. Первый разработчик этот класс просто в какой-то момент выбрасывает, второй на него реагирует и знает, что это надо отправлять на email, SMS, push-уведомления на телефон и все остальные миллион уведомлений с учётом всяких предпочтений пользователей, которые обычно бывают.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Вот то самое небольшое, но важное замечание. В статье Джимми используется перегрузка метода SaveChanges, и лучше этого не делать. А сделать это лучше в декораторе, потому что, если мы перегружаем метод SaveChanges и нам в Handler’е потребовался dbContext, мы получим циклические зависимости. С этим можно работать, но решения получаются чуть менее удобные и чуть менее красивые. Поэтому, если pipeline построен на декораторах, то смысла делать по-другому я не вижу.

Логирование и профилирование

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Вложенность кода осталась, но в первоначальном примере у нас был сначала using MiniProfiler, потом — try catch, потом — if. Итого было три уровня вложенности, теперь каждый этот уровень вложенности находится в своем декораторе. И внутри декоратора, который у нас отвечает за профилирование, у нас только один уровень вложенности, код читается отлично. Кроме того, видно, что в этих декораторах только одна ответственность. Если декоратор отвечает за логирование, то он будет только логировать, если за профилирование, соответственно, только профилировать, всё остальное находится в других местах.

Response

После того, как весь pipeline отработал, нам остается только взять Dto и отправить дальше браузеру, сериализовать JSON.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

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

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Не могу здесь не упомянуть ещё раз Скотта Влашина и его доклад «Railway oriented programming». Почему? Оригинальный доклад целиком и полностью посвящён работе с ошибками на языке F#, тому, как можно организовать flow немножко по-другому и почему такой подход может быть более предпочтительным, чем использование Exception’ов. В F# это действительно работает очень хорошо, потому что F# — это функциональный язык, и Скотт использует возможности функционального языка.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Так как, наверное, большинство из вас всё-таки пишет на C#, то, если написать аналог на C#, то этот подход будет выглядеть примерно следующим образом. Вместо того, чтобы выбрасывать исключения, мы объявляем такой класс Result, у которого есть успешная ветка и есть неуспешная ветка. Соответственно два конструктора. Класс может находиться только в одном состоянии. Этот класс является частным случаем типа-объединения, discriminated union из F#, но переписанный на C#, потому что встроенной поддержки в C# нет.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Самый простой способ их скомбинировать — использовать LINQ, потому что вообще-то LINQ работает не только с IEnumerable, если доопределить методы SelectMany и Select правильным образом, тогда компилятор C# увидит, что можно использовать для этих типов LINQ-синтаксис. В общем-то получается калька с do-нотации Haskell или с тех же самых Computation Expressions в F#. Как это следует читать? Вот у нас есть три результата операции, и если там во всех трёх случаях всё хорошо, тогда возьми эти результаты r1 + r2 + r3 и сложи. Тип результирующего значения тоже будет Result, но новый Result, который мы объявляем в Select’е. В общем-то, это даже рабочий подход, если бы не одно но.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Для всех остальных разработчиков, как только вы начинаете писать такой код на C#, вы начинаете выглядеть примерно вот так. «Это плохие страшные Exception’ы, не пишите их! Они — зло! Лучше пишите код, который никто не понимает и не сможет отладить!»

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

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

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

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

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Мы знаем, что, если с валидацией какие-то проблемы, надо вернуть код 400 или 422 (Unprocessable Entity). Если проблема с аутентификацией и авторизацией, есть 401 и 403. Если что-то пошло не так, то что-то пошло не так. А если что-то пошло не так и вы хотите сказать пользователю, что именно, определите свой тип Exception’а, скажите, что это IHasUserMessage, объявите в этом интерфейсе геттер Message и просто проверьте: если этот интерфейс реализован, значит, можно взять сообщение из Exception’а и передать его в JSON пользователю. Если этот интерфейс не реализован, значит, там какая-то системная ошибка, и пользователям мы скажем просто, что что-то пошло не так, мы уже занимаемся, мы всё знаем — ну как обычно.

Query Pipeline

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

Security

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

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

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

Проблема решается довольно просто. Мы можем доопределить интерфейс (IPermissionFilter), в который приходит оригинальный queryable и возвращается queryable. Разница в том, что к тому queryable, который возвращается, мы уже навесили дополнительные условия where, проверили текущего пользователя и сказали: «Вот этому пользователю верни только те данные, которые…» — а дальше вся ваша логика, которая связана с permission’ами. Опять же, если у вас есть два программиста, один программист идёт писать permission’ы, он знает, что ему надо написать просто очень много permissionFilter’ов и проверить, что по всем сущностям они работают правильно. А другие программисты ничего не знают про permission’ы, в их списке просто всегда проходят правильные данные, вот и всё. Потому что они получают на входе уже не оригинальный queryable из dbContext, а ограниченный фильтрами. У такого permissionFilter’а тоже есть свойство компоновки, мы можем все permissionFilter’ы сложить и применить. В итоге получаем результирующий permissionFilter, который максимально сузит выборку данных с учётом всех условий, которые для данной сущности подходят.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Почему это не делать встроенными средствами ORM, например, Global Filters в entity-фреймворке? Опять же для того, чтобы не городить себе всякие циклические зависимости и не тащить в context всякую дополнительную историю про ваш бизнес-слой.

Query Pipeline. Read Model

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

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Если мы пишем на C#, то, скорее всего, мы используем LINQ, если нет только каких-то чудовищных требований по производительности, а если они есть, то, возможно, у вас не корпоративное приложение. Вообще эту задачу можно решить раз и навсегда вот таким LinqQueryHandler’ом. Здесь довольно страшный constraint на дженерик: это вот Query, который возвращает список проекций, и она ещё может фильтровать вот эти проекции и сортировать вот эти проекции. Ещё она работает только с какими-то типами сущностей и знает, как эти сущности преобразовать к проекциям и вернуть список таких проекций уже в виде Dto в браузер.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Реализация метода Handle может быть такая, довольно простая. На всякий случай проверим, реализует ли этот TQuery фильтр для изначальной сущности. Дальше делаем проекцию, это queryable extension AutoMapper’а. Если кто-то до сих пор не знает, AutoMapper может строить проекции в LINQ, то есть те, которые будут строить метод Select, а не маппить это в памяти.

Дальше применяем фильтрацию, сортировку и выдаём всё это в браузер. Как именно всё это делается, я рассказывал в Питере на DotNext, это ещё один целый доклад, он уже выложен в свободный доступ и расшифрован на Хабре, можете послушать, посмотреть, прочитать, как написать с помощью expression’ов фильтрацию, сортировку и проекции для чего угодно один раз и дальше повторно использовать.

Не все выражения одинаково полезны транслируются в SQL

Двигаемся дальше. Одна тема, которую я не осветил на прошлом DotNext’е, — это проблемы с трансляцией в SQL. Потому что в Select мы, конечно, можем написать всё что мы хотим, но queryable-провайдеры не всё разберут.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Раз уж речь зашла про Хабр, давайте на примере Хабра. У нас есть список постов, у них есть Title, и Title мы хотим вводить как название хаба, а потом название самого поста. Вот с этой проекцией проблем нет, всё преобразуется. А вот если мы хотим вывести такой SubTitle, когда в последний раз обновляли статью, когда её создали, и хотим ещё использовать какой-то свой кастомный формат для этих дат, вот с этим queryable-провайдер уже не справится. Он не в курсе, что за кастомный формат объявлен в нашем коде.

И есть один довольно простой трюк, который эту проблему решает. Вместо того, чтобы пытаться сделать проекцию, мы делаем проекцию на примитивы. То есть вытаскиваем всё, что нам нужно, сначала. Далее помечаем это всё «JsonIgnore», чтобы сериализатор проигнорировал эти поля. И объявляем тот метод, который нам нужен, в Dto. То есть вместо того, чтобы делать это в проекции, мы это делаем уже в памяти. Когда сериализатор начнёт преобразование класса в JSON, он увидит, что Created и LastUpdated он должен пропустить, а SubTitle — это публичное свойство, надо его взять. Тогда он возьмёт его, вызовет этот метод, и дальше мы уже в памяти домаппим то, что нам нужно, то, что мы не смогли преобразовать в проекции. В большинстве случаев такой простой трюк решает проблему с тем, что какие-то выражения не могут быть преобразованы.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Давайте посмотрим на оба стека вместе. Они, в общем-то, довольно похожи и отличаются только тем, какие шаги мы собрали. В зависимости от того, какой pipeline, мы можем применять разные декораторы. Вот запросы мы будем кэшировать — а в командах это уже нам, допустим, не потребуется. Аналогично, команды мы хотим вызывать в SaveChanges, а в Query не надо вызывать SaveChanges. Когда пайплайны собраны и мы понимаем, что их ограниченное количество, такие декораторы можно взять и оформить в виде отдельных библиотек, положить на NuGet, и дальше просто подключать в виде повторно используемых модулей.

Потому что в коде декораторов нет ничего про домен. Вы можете отдельно писать домен, а инфраструктуру передать какому-то другому разработчику, которой оформит вам эти модули, и вы будете ими пользоваться. Если вы знакомы, например, с трудами Брукса, наверное, знаете, что самый простой способ написать код — это его купить. Соответственно, отличный вариант, если вы можете взять и сказать: «Нам нужны вот такие декораторы», — и их купить. Никакой ответственности.

Регистрация декораторов

Если декораторы такие замечательные, как же их регистрировать?

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Регистрировать их придётся как-то вот так. Не совсем красиво.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Хотя руками, конечно, никто это не делает, это всё происходит через контейнеры. Можно взять MediatR Джимми Богарда, в котором это всё уже есть и есть документация. Всё, о чём я рассказывал, такие же декораторы — правда, у него в MediatR это называется pipeline behaviour. Но смысл тот же, там так же определены методы Request/Response, RequestHandler’ы и методы для регистрации этих декораторов. А можно взять Simple Injector, у которого декоратор — это прямо фишка фреймворка.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

И сейчас вернёмся вот на этот слайд, помните, я говорил, что нам потребуется ещё раз этот дженерик, где TIn: ICommand.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Вот в Simple Injector’е поддерживается регистрация декораторов на основе constraint’ов дополнительно. То есть вы можете там, где будете регистрировать декоратор, указать, что если декоратор с constraint’ом, то он будет применяться только к тем Handler’ам, у которых есть такой constraint. Соответственно, если у нас есть constraint ICommand, мы можем сделать декоратор на SaveChanges тоже с constraint’ом на ICommand, и Simple Injector автоматически поймёт, что эти два constraint’а одинаковые, и будет применять этот декоратор только к соответствующему Handler’у. Ну, получается ещё одна маленькая красивая фишечка, которая позволяет на системе типов строить вот такую логику приложения, что к чему должно применяться.

Что использовать? Simple Injector или MeriatR — в принципе, на вкус и цвет все фломастеры разные, кроме того в Autofac’е, по-моему, тоже есть декораторы и в других контейнерах может быть тоже, я просто не слежу, не знаю. Если интересно, посмотрите.

Организация по модулям, а не слоям

Во всём моём текущем докладе не хватает ещё одного слова, чтобы кричать «бинго».

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Даже двух слов, а именно «Clean architecture». Нельзя же было упомянуть много умных людей и забыть про дядюшку Боба Мартина.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Современные веб-приложения почему-то очень любят рассказывать о том, что они MVC, какие они замечательные, какая у них структура.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Вместо этого, и Боб Мартин, и многие другие, и в том числе Angular, кстати, уже предлагает структурировать приложение на основе того, какие там есть модули в системе, то есть какая функциональность. Вместо того, чтобы сказать: «Я — MVC-приложение», можно сказать: «У меня есть следующие Features, то есть такая функциональность: у меня есть менеджмент аккаунтов, у меня есть Blog и у меня есть какой-то Import, то есть три каких-то больших модуля».

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

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Я же обещал всё-таки не давить авторитетами и тем, что кунг-фу сильнее другого кунг-фу, поэтому я приведу и другие преимущества такого оформления.

Во-первых, код в таком случае добавляется, а не редактируется. Если у нас есть разные модули и мы хотим добавить новый модуль, это новая папка. Не получится такого, что в модуле А и в модуле Б есть какая-то работа с юзерами, поэтому программисты Вася и Петя оба пошли исправлять User Service, дальше отправили pull request’ы, и тут внезапно случился конфликт, потому что они оба изменили этот User Service в соответствии с тем, как считали нужным. Причём даже без того, что они изменили сигнатуры или что-то вроде, а просто у них где-то поменялись строчки, типы. Какие-то банальные технические вещи могут приводить к тому, что на этапе код ревью может случиться конфликт и это затянет релизный цикл.

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

В практике нам раза два приходилось проводить такие действия — слово «рефакторинг», наверное, не совсем правильно, когда выбрасываешь весь код и заново переписываешь, это скорее рерайт. И если бы код был написан в обычном слоёном стиле, так бы не получилось: все эти сервисы, относящиеся к разным модулям, мы бы не смогли выкинуть, потому что были бы лишние зависимости. А так мы просто выкинули несколько косячных модулей и потом переписали, когда руки дошли. Я не буду вдаваться в подробности, почему так пришлось сделать, но иногда бывает. То есть это произошло не потому, что были плохие и глупые программисты, а потому что так сложились обстоятельства.

И последний момент: такое разделение упрощает работу численными методами и коммуникацию. Когда я говорю «численными методами», я опять же делаю реверанс в сторону менеджмента: мы начинаем считать количество фич, количество возвратов с код ревью, количество возвратов с тестирования и вот это вот всё. Помните, когда я формулировал критерии, обратил внимание на то, что довольно сложно отслеживать связь между регрессией, багами, которые дошли до продакшна, и тем, почему так произошло. А когда мы начинаем класть код таким образом, становится чуть легче. Потому что, если приходит какой-то pull request на редактирование существующих модулей, вариант номер один — изменились требования, вариант номер два — что-то пошло не так, баг пролез на продакшн. И вот дальше мы уже смотрим историю изменения в VCS именно по этому модулю: а что ж он пролез-то на продакшн, какие там коммиты были? Если эти коммиты находятся в этом модуле, в них ещё как-то можно разобраться, а если они просто размазаны по всем нашим слоям, разбираться становится сильно сложнее.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Несмотря на это, всё, о чем я говорил, не лишено недостатков. А именно: это не работает из коробки. То есть если вы возьмёте просто шаблон проекта, вам придётся дописывать инфраструктурный код. В идеале, написать свой шаблон проекта, в который уже будет подключено всё, что нужно, будет структура проектов. Но уходит на это, наверное, не меньше рабочего дня, если вы с нуля это делаете. Ну, один раз, допустим. Когда я говорю «рабочий день», это в смысле у нас уже всё готово, вам надо только зависимости собрать. На то, чтобы собрать зависимости, у меня ушло несколько лет — с тем, как изменялась моя мысль о том, как писать код.

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

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

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

Дальше этот IHandler расширяем двумя интерфейсами ICommandHandler и IQueryHandler и говорим, что это холистические абстракции. Очень круто звучащее словосочетание, значит на самом деле просто, что оно выполняется в рамках одной транзакции. То есть, если есть CommandHandler, внутри него не будет другого CommandHandler’а, он действует на протяжении всего этого запроса.

Почему так? Это исключает флейм на тему того, что можно там Query использовать в командах, команды в Query — вот это всё. Если вам нужен повторно используемый код, который придётся использовать и там, и там, тогда вы объявляете Hander, если вы объявляете CommandHandler или QueryHandler, это значит какой-то конкретный use case, это не должно повторно использоваться.

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

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

И мы всё ещё ждём C# 8, чтобы появился nullable reference type и наша система типов стала получше. Не такая, конечно, крутая, как в функциональных языках программирования, но лучше.

События можно трекать в рамках транзакции с помощью ChangeTracker’а ORM.

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

Я упомянул в докладе много людей и идей. Вот ссылки:

Последний слайд — немножко рекомендуемой литературы. Слева у нас нетленочка Эрика Эванса. Вторая книжка — это книга Скотта Влашина «Domain Modeling Made Functional», она про F#, но даже если вы не хотите никогда писать на F#, я всё равно её рекомендую прочитать, потому что она здорово структурирована, там очень чётко изложены мысли, просто с точки зрения здравого смысла и того, что два плюс два равно четыре. То есть идеи можно и на C# переносить, но за одним исключением, чтобы не выглядеть как на том слайде про Exception’ы.

И последняя, может быть, неочевидная книга — это «Entity Framework Core In Action». Я её здесь разместил не потому, что она про Entity Framework, а потому что там есть целый раздел про то, как использовать всякие варианты DDD с ORM, то есть то, где нам ORM начинает мешать в плане реализации DDD и как это обходить.

Блеск и нищета модели предметной области

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Мартин Фаулер в книге «Patterns of Enterprise Application Architecture» описывает «Модель предметной области (Domain Model)» как сложный подход к организации бизнес-логики. Метод заключается в создании классов, соответствующих объектам предметной области из реального мира как с точки зрения структуры данных, так и поведения. При этом технические аспекты, такие как хранение данных, аутентификация и авторизация, управление транзакциями, выносится за пределы слоя бизнес-логики. Паттерн реализуется одним из двух способов:

Структура поста:

Историческая справка

Впервые я столкнулся с термином «модель предметной области» (Domain Model) читая книгу Patterns Of Enterprise Application Architecture (PoEAA) Мартина Фаулера. Прочитал и немного чего понял. Может быть время было неподходящее, а может у Фаулера было написано уж очень кратко. Так или иначе, после прочтения паттерн был забыт на несколько лет, пока в руки ко мне не попала небезызвестная «синяя книга» Эрика Эванса Domain Driven Design. На этот раз я по настоящему проникся, как в фильме «Матрица», когда Нео смогу наконец освободить свой разум. Не в том смысле, конечно, что начал останавливать пули силой мысли или обрел какие-то иные сверхъестественные способности. Вместо этого в значительной изменились мои взгляды на то, что важно, а что не очень в контексте разработки корпоративного ПО. До прочтения я считал единственно-важным технологический аспект, а после — закрались мысли, что нужно еще заниматься аналитикой, сбором требований и другими немаловажными вещами, а главные сложности в корпоративной сфере вообще связаны с людьми, а не технологиями.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Однако, далеко не все разделяют мой щенячий восторг по поводу Эванса, многим больше нравится «красная» книжка Вернона. На вкус и цвет, как говорится, все фломастеры разные, так что какую из них читать каждый решает сам. Можно читать и обе, но их содержание в значительной степени пересекается. А вот книга Скотта Влашина Domain Modeling Made Functional вышла буквально в прошлом году. Она примечательна тем, что рассматривает типовые проблемы предметно-ориентированного проектирования через призму функционального программирования и дает некоторые неожиданные ответы, недоступные в ООП.

Например, в ООП исторически сложилось два основных подхода к моделированию домена: «богатая» и «бедная или анемичная» модели. В ФП же, этого разделения нет, потому что функциональные языки отличаются по возможностям от классических ООП-языков и потому что в функциональном мире вообще не принято объединять структуру данных и операции над ними (поведение) в объекты. В контексте доклада я буду в основном оставаться в объектно-ориентированной парадигме и лишь пару раз соскользну на кривую тропинку функциональщины.

Богатая

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Богатая модель — это способ моделирования, который имели в виду Фаулер и Эванс:

Анемичная

В анемичной, все ровно наоборот.

Причины раскола

Сложно сказать в какой именно момент произошел этот раскол и что стало тому причиной. Лично я считаю, что основных причин две:

Засилье ORM

Давайте посмотрим на статистику скачивания пакетов с nuget.org. Entity Framework скачивают чаще, чем ASP.NET MVC. Можно предположить, что множества скачивающих ASP.NET MVC и Entity Framework в значительной степени пересекаются, и сделать вывод, что многие веб-приложения манипулируют данными посредством ORM. На сколько это действительно нужно делать — вопрос открытый.

Простота реализации

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

Always valid (миф или реальность?)

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf Можно предположить, что проблема кроется исключительно в недостаточной квалификации программистов. Почему же тогда таких «неумех» так много в индустрии? Почему люди отказываются от столь заманчивых идей, как «писать код, из которого понятны бизнес-правила» и «всегда соблюдать инвариант»?

Ведь что значит стопроцентное соблюдение всех инвариантов? В пределе — то, что ни один объект вообще нельзя создать в «неправильном состоянии». А это значит, что ошибки будут найдены не на этапе юнит-тестирования, приемочного тестирования или, упаси господи, на продакшене, а в момент компиляции.

Более глубоко тема инвариантов и проектирования, направленного на исключение возможности ошибки во время компиляции программы, а не во время выполнения в статье Скотта Влашина «Making illegal states unrepresentable»

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf Кто вообще в здравом уме будет отказываться от такого? Не жизнь, а сказка… или миф… а может быть художественный вымысел?

Такая дискуссия состоялась в интернете между Грегом Янгом и Джеффри Палермо. Первый — сторонник концепции Always Valid, а второй утверждал, что этот подход вообще неосуществим в реальности.

Аргументы Джеффри вполне логичны. Для обеспечения корректности состояния любого объекта в изменяемой (mutable) среде нам придется снабдить любой setter защитной конструкцией, например такой.

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

Давайте оставим пока SOLID в покое и посмотрим на эту аргументацию с другой стороны. В некоторых случаях обеспечить корректное состояние объектов может быть просто невозможно. Коммерсанты давно смекнули, что вероятность покупки в интернет магазине снижается в зависимости от количества необходимых форм для заполнения. В идеале вообще должна быть только одна огромная кнопка «купить» и какой-то способ связаться с пользователем. Остальные «необходимые» поля может заполнить кол-центр после получения оплаты.

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

Таким образом, в одних случаях нужно, чтобы обязательных полей было минимум, а в других — максимум. Значит в разных контекстах правила «обязательности» полей класса могут быть разными и экземпляр класс вообще не может всегда находиться в «правильном» состоянии, потому что «правильность» зависит от текущего контекста. Шах и мат, Грег Янг?

Проблема универсалий

Удивительно, что впервые в известной человечеству истории подобными вопросами задались Платон с Аристотелем задолго до становления кибернетики с информатикой и изобретения компьютеров. Я не буду сильно углубляться в онтологию, да просят меня древнегреческие философы за весьма вольное толкование их идей.

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

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Здесь не все так очевидно. Это персонаж мультфильма «Гадкий я 3», на протяжение которого одна маленькая девочка искала живого единорога, потому что единорог слева был ее любимой игрушкой… и в общем и целом она нашла. Какое отношение древнегреческие философы и современные мультфильмы имеют к контекстной валидации? Как ни странно, самое прямое.

Контекстная валидация и инвариант

Давайте разделим валидацию на два подвида:

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

Паттерны DDD на практике

DDD-жаргон

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

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

Таким образом, существует три заявки: на оплату, изготовление и доставку, обладающие разными характеристиками и имеющие смысл только внутри ограниченного контекста, а не всего предприятия. Словарь терминов, понимаемый одинаково в рамках ограниченного контекста называется единым языком. Внутри ограниченного контекста DDD предлагает три основных инструмента моделирования: Value Object, Entity и Aggregate.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Агрегаты — это деревья объектов, обладающие инвариантом для группы, а не для единичного объекта. Доступ к агрегатам осуществляется через «Корень агрегации» — объект, находящийся в корне дерева. Таким образом, корень обеспечивает инвариант всей группы с помощью инкапсуляции.

Сущности и Value Object — это основные строительные блоки приложения, которые могут как входить в агрегаты, так и не входить. Их основное отличие в том, что у сущностей есть уникальный идентификатор, а у объектов-значений — нет.

Дизайн на основе типов

Вернемся к пользователю интернет магазина. Попробуем смоделировать всего один объект в стиле «богатой» модели. Мы пришли к тому, что валидацию инварианта и контекстную валидацию необходимо разделить. Самый простой способ достижения цели — разделить класс, моделирующий пользователя. Напрашивается два основных подтипа:

Подробнее этот подход в докладе Скотта Влашина Domain Modeling Made Functional или нашего с Вагифом Абиловым Жизнь после бизнес-объектов.

Вынести IO на границы (Anticorruption Layer)

Следующим шагом перенесем ввод-вывод на границы приложения. Данные, пришедшие извне по определению могут быть в любом состоянии: как в согласованном, так и нет. DDD даже предлагает специальный паттерн для пограничного контроля ограниченных контекстов — Anticorruption Layer, который, впрочем, отличается от обычного фасада лишь более узкой специализацией.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

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

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-список, а операции записи осуществлять с помощью специализированных методов, содержащих необходимые проверки. И с этого момента начинаются первые проблемы.

Паттерн Агрегат — очень хорошо выглядит на бумаге, но оказывается весьма неуклюжим, когда дело доходит до практики. Агрегаты по своей природе сложнее чем сущности или объекты-значения, потому что представляют собой не один объект, а целое дерево. Поэтому проблемы, которые казались на уровне сущностей незначительными, становятся весьма неприятными по мере роста дерева объектов и возможных комбинаций состояний. Рассмотрим типовой сценарий работы с заказом: проверка наличия на складе, отправка, отмена. Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

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

Данные и методы, связанные с соответствующими состояниями заказа имеют смысл, только в рамках одного состояния. Мы же снова объединили все в одном классе и получили пусть и вкусный, но все-таки винегрет. Поэтому счет сравнялся — 1:1. Напоминаю, анемичная модель никогда не утверждала, что будет соблюдаться инвариант, тем более для группы объектов и не обещала того, что из программного кода будут понятны бизнес-правила, поэтому в рамках анемичной модели претензий к дизайну нет, а к богатой появились вопросы. Где выразительность? Где статический анализ для бизнес-правил? Его нет, по крайней мере в классических объектно-ориентированных языках. Зато такая возможность есть в функциональных ЯП с более сильной системой типов.

В F# все иначе

В F# существуют так-называемые алгебраические типы данных: records (да-да, те самые, что завезут в C#9) и discriminated union. Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Поддержки discriminated union в C# в ближайшее время не планируется. Можно воспринимать их как enum на стероидах. В отличие от классического enum-а в перечислении discriminated union могут входить другие типы, в т.ч. и records. Именно поэтому, такая система типов называется «алгебраической». Record — это тип «и &», а discriminated union 0 это тип «или |». Таким образом все приложение может быть построено за счет комбинирования маленьких типов одним из способом. Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdfВ отличие, от привычного в ООП control flow, основанного на полиморфизме, в ФП часто используется передача управления с помощью pattern matching. Для каждого подтипа в discriminated union необходимо написать свою ветку выполнения, в которой будут доступны только данные и поведение, имеющее смысл в рамках в данного состояния объекта.

Эта проблема решается и без применения F#, однако в C# решение выглядит менее элегантным. В статье Шаблон проектирования «состояние» двадцать лет спустя имитируется функциональный подход на основе классического ООП-паттерна с применением современных языковых конструкций C#. На момент написания статьи switch expression еще не зарелизили. С ним pattern matching выглядит лучше. Проконтролировать разбор всех наследников можно написав свой анализатор Roslyn.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdfНесмотря на то что мне пришлось сменить язык программирования, я считаю что можно выдать богатой модели еще одно очко. Счет становится 2:1. F# совместим с C# и поддерживает объектно-ориентированную парадигму, поэтому используя только F# или F# в сочетание с C# можно решить проблему разного поведения в разных состояниях. К сожалению, радоваться еще рано. У меня в запасе есть еще несколько сценариев проблематичных сценариев.

Домен и инфраструктура

Распределенные транзакции

«Классическое фаулеро-эвансвое» DDD настаивает на том, что инфраструктура и домен должны быть разделены. А что делать, если инфраструктура становится частью домена. Как так? Легко. Например системы документооборота. Представьте, что вам нужно загружать и подписывать цифровыми подписями, а затем парсить и работать с данными из сотни тысяч документов. Каждый раз открывать бинарные файлы — не вариант. Поэтому вам потребуется механизм, гарантирующий консистентность данных в бинарных файлах и в реляционной структуре при добавлении новых или редактировании старых документов. Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

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

Internal

Можно делегировать это разработчикам, но где гарантия того, что каждый из разработчиков окажется достаточно ответственным и не забудет что-то обработать? Мой опыт показывает, что таких гарантий дать нельзя. Счёт сравнивается 2:2.

Я считаю, что единственный способ что-то гарантировать — это отобрать возможность выбора. Если public заменить на internal, положить эти объекты в одну сборку, а публичный API предоставить через специализированные методы сервисов, то куда вы денетесь с подводной лодки?

Сервисный слой

Для обновления документа мы:

Теперь все будут использовать именно этот метод. Я предпочел бы этот страшный зашумлённый код никогда больше не писать и оставить здесь — чтобы он был только в одном сервисе, и ключевое слово internal нам помогает ровно так и сделать.

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

Все вместе

Главный вопрос DDD, смысла жизни и всего такого?

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

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

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Действительно, 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 плохой инструмент для этой задачи, поэтому просто возьмите другой инструмент.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Как только вы начинаете возводить всё в абсолют и говорить «нет, мы не будем писать так код, потому что Фаулер Эванс, Вернан — кто угодно — сказал, что „нельзя“, вы обязательно будете испытывать только расстройство. Только эти ребята (на слайде персонажи фильма „Звёздные войны“) возводят всё в абсолют. Поэтому, несмотря на все оговорки и не самое высокое качество тех решений, которые я предложил, давайте считать, что счёт у нас равный — 3:3.

Workarounds

Вернёмся к агрегатам: независимо от того, анемичную или богатую модель мы используем, большие агрегаты никуда не делись. То есть когда вы тащите половину базы данных, чтобы что-то посчитать, по умолчанию такой подход будет менее эффективным, чем просто выполнить запрос к базе данных. Ответить на рукописный запрос всегда будет эффективней, потому что не надо тащить данные по сети, поднимать их в оперативную память и там считать. База данных это делает внутри своего приложения. Значит ли это, что для определённого класса задач объектная модель никак не подходит? Если бы мы работали с Java, я бы сказал „да, так и есть“.

Expressions

Specification

Паттерн „Спецификация“ раньше работал только с объектами и создавал проблемы с производительностью, потому что нам нужно было сначала вытащить весь набор данных, а потом в оперативной памяти его отфильтровать. В C# этот паттерн обретает новую жизнь: если у нас есть правило о том, что для продажи только определённые товары, у которых цена больше нуля, мы можем объявить это правило как Expression.

Оно выглядит как C# код, соответственно, оно может быть частью нашей модели домена, но мы его никогда не выполняем как C# код — мы его используем для трансляции к запросу к базе данных. Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdfПри этом мы получаем довольно эффективные запросы. Также если ваши объекты, агрегаты или сущности слишком большие, не обязательно их читать целиком, можно, используя C# и проекции, читать только часть этих данных в виде DTO и делать более производительные программы. Причем необязательно даже делать анонимные типы. Если вы используете AutoMapper или Mapster, можно вообще снизить количество императивного кода и заменить его на декларативный.

Default Interfaces Implementation

При этом возникают некоторые интересные лазейки, которые именно в классической ООП-парадигме не были бы возможными. Начнём с того, что с 8-ой версией C# у нас появились дефолтные реализации интерфейсов.

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

Теперь этот интерфейс можно „прилепить“ как к сущности, так и к DTO. Непосредственно код будет находиться ни в том, ни в другом объекте, а в реализации интерфейса.

Bulk Extensions

А может F#?

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Однако как только я начинаю использовать некоторые такие лазейки, я задумываюсь: на том ли языке программирования я сейчас пишу и тот ли инструмент я использую? Потому что дефолтная реализация интерфейсов — это, фактически, функция, которую мы можем „прилеплять“ к любым типам данных, используя интерфейс. Стоит ли переходить на F#, чтобы реализовать модель домена — каждый решает сам, исходя из потребностей проекта. Мы так и не решились переезжать. Тем не менее, поглядывать в сторону каких-то других языков программирования бывает полезно, чтобы позаимствовать оттуда некоторые идеи.

Bounded Context

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

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Ложный агрегат

Я говорил о том, что агрегаты имеют свойство разрастаться. Это происходит, когда мы используем документацию Microsoft и следуем ей вслепую. Например, когда мы хотим объявить связанные коллекции, мы обычно связываем их в две стороны. А вот если пользователь может много чего делать в вашей системе, тогда и объект User будет довольно-таки большим. Это антипаттерн „Божественный объект“.
Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf
И ладно, если бы он только тратил ресурсы памяти — это мы можем оптимизировать с помощью Expressions. Такой подход ещё и порождает большое количество циклических зависимостей, потому что сейчас у заказов тоже может быть набор пользователей, у комментариев может быть набор товаров, и так далее.

Пока все эти объекты живут у вас в одной сборке, вы получаете такие „круговые“ зависимости: A зависит от B, B зависит от C, C зависит от A. В итоге получается большой и страшный монолит, который мы никак не можем распилить. Всё дело в том, что этот агрегат ложный, в реальном мире его не существует, потому что я объединил в класс пользователя всё, что только можно.

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

Первый список вопросов (организационный и никак не связан с технологиями)

Второй список вопросов (технологический)

Как делить Domain Model на Bounded Context

Как же делить Domain Model на Bounded Context? Я уже сказал, что в итоге внутри контекста у нас будут сущности и агрегаты. Сущности редко имеют тенденцию расползаться, потому что они маленькие, а пример „божественного“ агрегата я приводил до этого. И если вы видите, что агрегат залез между двумя контекстами, значит, он неправильный. Но это правило работает и в обратную сторону: если вы уверены, что агрегат правильный, и ответ на вопросы выше — „да“, то, похоже, вы неправильно нарезали контекст. Поэтому ответ на вопрос „как же нарезать Domain Model на Bounded Context?“ — по границам агрегатов.

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Монолит на микросервисы

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

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Но, тем не менее, ответ такой: мы растащили Domain Model на разные Bounded Context, сказали, что Bounded Context — это отдельный микросервис и получили из страшного монолита много маленьких красивых микросервисов. Причём, обратите внимание, все проблемы, связанные со страшными монолитами, которые не распиливаются, чаще всего связаны с тем, что у вас есть циклические зависимости, которые сложно растащить. Если в самый начальный момент этих зависимостей нет, значит и разделить приложение на несколько процессов будет сильно проще.

Как пересечь границу контекстов

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

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Классическое решение такой ситуации — использование событий. События могут быть сериализованными и пересекать границы контекстов. Как именно „кидаться“ событиями — отдельная история. Если это микросервис, значит, вам потребуется, скорее всего, шина. Если это одно приложение, то это можно сделать и в памяти. Так или иначе, вы закончите тем, что у вас будет диспетчер, который будет слушать все эти события и направлять их по типу на разные обработчики. Если вам требуется реагировать на какие-то события из одного контекста в другом, то „выкидывайте“ события и обрабатывайте их в том контексте, в котором вам нужно.

Что выбрать?

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf

Эволюционный рефакторинг

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

Полезные ссылки

Несмотря на то, что мы уже долго разбираем DDD, я рассказал, наверное 10% того, что вообще есть на эту тему. Поэтому я выбрал материалы, которые хорошо дополняют мой рассказ.

По-русски

In English

В этом году я буду MC (Master of Ceremonies) на конфереции DotNext 2020 Piter, которая пройдет с 15 по 18 июня. Благодаря онлайн-формату в этом году можно будет задать вопросы известным спикерам, таким как Scott Hanselmann и Jon Skeet.

Спасибо indienkova за помощь в подготовке материала. Без нее расшифровка могла не выйти или выйти значительно позже.

Functional and Reactive Domain Modeling

Domain modeling made functional pdf. Смотреть фото Domain modeling made functional pdf. Смотреть картинку Domain modeling made functional pdf. Картинка про Domain modeling made functional pdf. Фото Domain modeling made functional pdf
Название: Functional and Reactive Domain Modeling
Автор: Debasish Ghosh
Издательство: Manning Publications
Год: 2016
Страниц: 325
Формат: PDF
Размер: 19 Mb
Язык: English

Functional and Reactive Domain Modeling teaches readers how to think of the domain model in terms of pure functions and how to compose them to build larger abstractions. It begins with the basics of functional programming and gradually progresses to the advanced concepts and patterns needed to implement complex domain models. The book demonstrates how advanced FP patterns like algebraic data types, typeclass based design, and isolation of side-effects can make models compose for readability and verifiability.

On the subject of reactive modeling, the book focuses on higher order concurrency patterns like actors and futures. It uses the Akka framework as the reference implementation and demonstrates how advanced architectural patterns like event sourcing and CQRS can be put to great use in implementing scalable models. It offers techniques that are radically different from the standard RDBMS based applications that are based on mutation of records. It also shares important patterns like using asynchronous messaging for interaction based on non blocking concurrency and model persistence, which delivers the speed of in- memory processing along with suitable guarantees of reliability.

Источники информации:

Добавить комментарий

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