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

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

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

Раньше мы в 3 частях [1, 2, 3] пробежались по основным идеям архитектуры систем. Поэтому, если вы ищете информацию по System Design, микросервисам и топологии команд, то вам туда. Эта же статья про архитектуру внутри кодовой базы: она посвящена концепциям программирования, влияющим на структуру приложения, поэтому описывает не только архитектурные подходы, но и иные идеи, оставляющие на дизайне свой отпечаток.

Дисклеймер

Это очень горячая тема для обсуждения: по разные стороны баррикад мы видим фанатов «Чистой архитектуры», DDD, функциональщиков, ООПшников, реактивщиков и поклонников иных взглядов на то, как, в итоге, должна выглядеть кодовая база. Предвкушая определенный негатив в комментах, я вынужден предостеречь читателя: мне не известны достаточно убедительные доказательства того, что тот или иной подход в проектировании приложения как‑то надежно коррелирует со стоимостью разработки, стабильностью или еще чем‑то важным. В отличие от архитектуры систем, тут я видел лишь пространные рассуждения и исследования из разряда «бабка надвое сказала». Так что вряд ли можно сказать, что структура приложения прям однозначно очень важна, а потому рекомендую не воспринимать все сказанное ниже близко к сердцу.

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

Что в меню

Попробуем вкратце разобрать основные идеи последних лет 70. Заодно, рассмотрим основные архитектурные инструменты, позволяющими решать конкретные проблемы.

Пробежимся очень кратенько по основным идеям в хронологическом порядке:

  • Функциональное программирование

  • ООП

  • Структурный дизайн

  • Шаблоны проектирования

  • Рефакторинг

  • YAGNI, KISS

  • PoEAA: Transaction Script и Domain model,

  • ORM

  • DDD

  • Hexagonal Architecture

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

50-е. Функциональное программирование

На заре компьютерной эпохи основными теоретиками в области вычислений были математики. И вот один из них, Джон Маккарти, придумал подход к написанию программ, где программа должна была представлять из себя композицию функций в математическом их смысле. То есть, вся парадигма вращается вокруг функций и их композиции. Простейшим примером такой функции будет всем известная парабола y(x) = x2.

В целом, идея очень логичная, если посмотреть, какие задачи решали тогда компьютеры. Вот типичные из них:

  • расчет баллистических таблиц для артиллерии,

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

  • расчет аэродинамической модели реактивного истребителя.

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

Спустя десятилетия начала осознаваться ценность одной интересной особенности функционального программирования - неизменности результата функции. Обратите внимание на параболу: эта функция всегда будет возвращать на один и тот же x один и тот же y. И вот это свойство отнюдь не гарантировано в иных подходах к программированию.

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

В целом, в современном хайпе вокруг функционального программирования лежат именно эти его преимущества:

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

  • простота написания многопоточного кода из-за отсутствия гонок при изменении состояния.

А будут минусы?

Да, конечно! Строгие правила написания в рамках этой парадигмы приводят к разным казусам.

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

    fac
    fac
  2. Куда сложнее история с тем, что функциональный код часто весьма непросто читать непривычным взглядом. И проблема тут далеко не в последнюю очередь в человеческом факторе, где новоиспеченные фанаты пытаются на полную катушку использовать все штучки-дрючки из мира ФП. Но последнее - это уже не столько проблема ФП, сколько вообще всего в нашей индустрии.

  3. Сложность реализации тривиальных вещей может быть избыточна. Тут я про append-only решения типа ивент-сорсинга.

  4. Распространённость ФП небольшая, а следовательно делать ставку в карьере исключительно на них нужно только из очень большой любви к искусству.

70-е. Структурный дизайн

В 70-е появилась концепция структурного дизайна, которая до сих пор являются фундаментом теории дизайна программного обеспечения. В оригинальной статье вводятся 2 главных термина: coupling и cohesion.

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

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

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

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

60е. ООП

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

  1. Данные и использующее их поведение в одном месте - это признак высокой плотности зависимостей, то есть cohesion. С плотно скомпонованным кодом гораздо проще работать, чем с развесистой лапшой.

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

  3. Главная фишка ооп - полиморфизм. Этот механизм позволяет избежать дублирования в коде там, где иные подходы уже не справятся. А для самых запутанных случаев можно даже подключить шаблоны проектирования, которые как раз для ООП разработаны и были.

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

80е. Шаблоны проектирования

В 80е годы появилось сообщество фанатов ООП, Smalltalk community, из которого выросли потом очень важные для нашей индустрии люди и не менее важные идеи: паттерны, рефакторинг, Agile, TDD/BDD, DDD, CI, CD и ещё много чего интересного. Так вот с паттернов их творческий путь, по сути, и начался.

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

Идея собрать шаблоны для решения задач была заимствована Кентом Беком из сферы строительства. Первые паттерны были настолько специфичны, что ими, наверное, сейчас никто и не пользуется. Зато дальше появляется целое движение шаблонов, а паттерны льются, как из рога изобилия, как, например, всем известные GoF-паттерны. Но ещё чаще сейчас используются широко известные паттерны, которые были описаны в не столь известных книгах, как PoEAA, DDD, CD и т.п.: domain model, value, entity, repository, ORM, service layer, deployment pipeline и ещё много чего.

Конец 80х. Рефакторинг

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

Обычно это были 4 многомесячные фазы:

  • сбор требований

  • проектирование

  • написание кода

  • тестирование.

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

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

И вот в 90-м году William Opdyke и Ralph Johnson (тот самый из GoF) публикуют статью по дисциплине изменения кодовой базы посредством последовательности очень небольших шагов. Эту дисциплину они впервые назвали рефакторингом. В 95-м эта дисциплина стала основным способом проектирования системы на первом Extreme Programming проекте — Chrysler comprehensive compensation system. Впервые дизайн системы проектировался не заранее, а эволюционировал с ходом разработки.

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

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

Описаны также основные методы рефакторинга — способы устранения этих лишних coupling‑связей и повышения того самого cohesion и читабельности. Скорее всего, в вашей среде разработки эти методы даже встроены: это те самые extract method, inline variable и прочее, что было добавлено во все IDE после публикации этой книжки. Впрочем, этим всем ещё надо уметь пользоваться, но это уже совсем другой разговор. Для этого вам надо сюда.

90е. YAGNI

В 90-е стартанул первый Extreme Programming проект, который я уже упомянул. Главная идея, которая стояла за экстремальным программированием — взять все хорошее и довести до максимума, до экстремума:

  • вместо трехмесячной дизайн фазы постоянный рефакторинг;

  • вместо многомесячной фазы тестирования — прогон автотестов несколько раз в день;

  • вместо одного релиза раз в год — поставки раз в 2 недели;

  • вместо периодического код‑ревью — парное программирование;

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

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

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

Минимализм проявлялся в постоянной чистке кода посредством рефакторинга от всего, что не было необходимо для прохождения тестов. Применялось правило YAGNI — you aren't gonna need it.

У Кента Бека было 4 требования к коду:

  • проходит тесты

  • раскрывает намерение

  • нет дублирования

  • наименьшее число классов и функций.

Нулевые. PoEAA

В начале нулевых Мартин Фаулер выпускает книгу Patterns of Enterprise Application Architecture, которая, на самом деле, во многом очень быстро устареет. Но там появляется очень важная мысль, которая актуальна до сих пор. При разработке приложения вам придётся выбрать между двумя подходами: Transaction script и Domain model.

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

Domain model — это подход, в котором вы уже моделируете свою предметную область в коде. Вы работаете не просто с данными и вводом‑выводом. Теперь вы делаете модель предметной области, которая сама должна контролировать свою консистентность и, где нужно, абстрагировать свои детали посредством интерфейсов.

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

В этой дихотомии Transaction Script проще, и его стоит выбрать, если система простая и сложнее становиться ТОЧНО не будет. Ее написали за пару недель и больше не трогают и трогать не будут.

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

ORM

В томже PoEAA появляется описание нового паттерна: object mapper, он же object‑relation pattern (ORM). Взрыв популярности ООП в девяностые показал сложность раскладывания объектов по таблицам: реляционные представления и объектные представления данных друг на друга ложились не очень хорошо и требовали специальных абстрактных мапперов для перекладки. Довольно скоро такие ORM появились уже в открытом доступе и быстро получили свою популярность.

Но лет через 10 за ними придёт и ORM hate — движение по отказу от ORM из‑за проблем со сложностью и непредсказуемостью таких фреймворков под нагрузкой.

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

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

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

Нулевые. DDD

Упоминая шаблон Domain Model, нельзя обойти вниманием книжку Domain Driven Design 2003 года за авторством Эрика Эванса. Именно в ней впервые вводятся шаблоны entity, value, aggregate и repository.

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

Нулевые. Hexagonal Architecture

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

Оригинальное изображение из оригинальной статьи
Оригинальное изображение из оригинальной статьи

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

  • контроллеры и клиенты для REST,

  • репозитории для баз данных,

  • консьюмеры и продюсеры для брокеров сообщений и т. п.

Таким образом, оставшийся код представляет из себя абстрактное «приложение» (Application‑layer в оригинальной терминологии), которое работает независимо от выбора технологии и реализации адаптера.

Простенький пример
Простенький пример

Приложение в такой парадигме само собой раскладывается в трехуровневую структуру: на первом уровне — входные адаптеры (контроллеры, консьюмеры, CLI, шедулеры и т. п.), на втором уровне — ядро или «приложение», на третьем — исходящие адаптеры (репозитории, клиенты, продюсеры).

Автор идеи декларировал ценность такого подхода, в первую очередь, в написании «быстрых» тестов на бизнес‑логику ядра в «приложении»: можно обойти медленные адаптеры первого уровня и заменить быстрыми фейками адаптеры третьего уровня. Такие тесты будут быстрее интеграционных даже с in‑memory базой данных в десятки раз, позволяя прогонять весь сьют в сотни тестов за пару секунд, что буквально незаменимо для рефакторинга. Того самого рефакторинга, который последовательность маленьких атомарных изменений структуры без смены поведения, а не того, что сейчас под этим словом часто понимают.

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

Небольшой пример из опыта нашей команды

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

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

Обратите внимание, что мы поменяли только третий адаптер - больше ничего.
Обратите внимание, что мы поменяли только третий адаптер - больше ничего.

На самом деле, такой пример, отнюдь не уникален. После успеха этого решения по нашим стопам пошла и команда, делающая UI: заколебав другую команду своими запросами на предоставление апишек и их оптимизацию, они тоже перешли, в итоге, на event‑driven подход на основе стриминга со своим хранилищем.

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

Собираем воедино

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

Повторюсь, мне не известны какие‑либо источники, надежно подтверждающие хоть какое‑то влияние структуры приложений хоть на что‑то кроме комфорта разработчика, так что то, что я сейчас буду рассказывать — это однозначная отсебятина и вкусовщина. Мы в своей команде делаем так (и тоже с вариациями). Если кому‑то еще пригодится — буду только рад.

Сгруппируем эти идеи и рассмотрим вопрос архитектуры приложения с 3 сторон:

  • идеология — ФП, ООП, шаблоны;

  • процессы — Рефакторинг, YAGNI, Структурный дизайн;

  • структура — DDD, Hexagonal Architecture.

Идеология

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

Из функционального программирования я бы порекомендовал взять уважение к неизменяемым структурам данных и избегание сайд‑эффектов, где это возможно (например, CQS).

Вынужден предостеречь, это может вас подтолкнуть к append‑only и event‑sourcing подходам, которые хоть часто и бывают полезны, но далеко не везде себя оправдывают, а потому могут сильно усложнить без особой надобности вашу архитектуру.

У ООП свои очень серьезные преимущества, которые я рекомендую не игнорировать. Повторюсь, эти преимущества:

  • высокий cohesion за счет хранения данных и поведения в одном месте

  • хорошие инструменты инкапсуляции модели, защищающие вашу модель от некорректного состояния

  • полиморфизм, позволяющие устранять дублирование там, где в ином случае вы были бы обречены.

Ну и я давно подозреваю, что половина GoF Design Patterns в языках без полиморфизма вряд ли возможна. А с ООП у вас есть целая книжка с полезными костылями под каждую конкретную проблему.

Процессы

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

Я очень сильно рекомендую строить вашу архитектуру в эволюционной парадигме:

  • избегать продолжительного проектирования до разработки и преждевременных оптимизаций

  • стараться вывести структуру приложения и его основные компоненты в процессе разработки, наблюдая за кодом и связями в нем

  • удалять все, что оказывается лишним

  • постоянно рефакторить с целью повышения читабельности кода.

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

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

Это итеративный процесс: сначала немного поведения, поэтом небольшой рефакторинг с приведением структуры к приемлемому виду, основываясь на код‑смеллах, каких‑то идеях из DDD и собственных представлениях о добре и зле. В нашей команде этот процесс является частью цикла Red‑Green‑Refactor.

Структура

Мы в своей команде основываем нашу архитектуру на DDD и Гексагональной архитектуре. Для этого выделяем 3 основных слоя: адаптеры, приложение и домен.

Простенький пример того, как мы используем гексагоналку. Видно: тоненький сервисный слой, на границе между слоями, как правило, доменные объекты. Не видно особо доменного поведения: подразумевается, что оно спрятано под методами доменной модели. В реальном коде у нас были бы вопросы по поводу 2 мест тут. Первое: где тестируется валидация продукта? Если в тесте адаптера, то это не очень хорошо. Второе: почему за personalData мы ходим через REST? Но это, может быть, навязанное снаружи решение. А, может, нам надо хранить эти данные у себя.
Простенький пример того, как мы используем гексагоналку. Видно: тоненький сервисный слой, на границе между слоями, как правило, доменные объекты. Не видно особо доменного поведения: подразумевается, что оно спрятано под методами доменной модели. В реальном коде у нас были бы вопросы по поводу 2 мест тут. Первое: где тестируется валидация продукта? Если в тесте адаптера, то это не очень хорошо. Второе: почему за personalData мы ходим через REST? Но это, может быть, навязанное снаружи решение. А, может, нам надо хранить эти данные у себя.

Домен

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

Код попадает в доменный слой из слоя приложения. Происходит это через рефакторинг: выделяются куски кода, которые удаётся вынести, и переносятся в доменные сущности. Самый лучший ориентир, куда надо что‑то положить — код‑смелл Feature Envy: если в параметрах функции есть доменный объект, то это хороший повод сделать её методом этого объекта.

Что не рекомендую:

  • Анемичная доменная модель

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

    В целом, анемичная доменная модель — это Transaction Script с издержками доменной модели, худшее из двух подходов.

  • Нарушение изоляции модели

    Очень часто приходится видеть модели, в которые протекла логика других уровней: это и код ORM, и запросы какие‑то, и ActiveRecord паттерн, иногда даже какие‑то DTO или иные детали внешних контрактов. Этот код явно не относится к предметной области, а, следовательно, скорее лишний.

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

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

Адаптеры

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

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

Обычно на выходе и на входе из адаптеров мы используем доменные объекты, которые маппятся на объекты адаптеров: ORM‑сущности или DTO.

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

  • для контроллера — это сам контроллер, дтошки, код отлова исключений;

  • для Hibernate‑хранилища — класс хранилища, репозиторий, сущности.

Приложение

Между адаптерами и доменом в самом центре находится слой приложения. Это тонкий сервисный, предназначенный для оркестрации работы модели и адаптеров. В идеале в слое приложения не должно быть почти никакой логики. Хороший пример: взяли доменный объект в адаптере, сделали манипуляцию, положили в адаптер. При этом всю сложность надо пытаться упрятать в домен. Понятное дело, в реальности бывает по‑разному, но эта мысль должна быть ориентиром.

Как понять, что код принадлежит этому слою? Он будет актуален только в контексте вашего приложения и вашей компании. Например, вы пишите Яндекс‑Такси, и тут вас окружает один набор команд и микросервисов, тогда как в СитиДрайве это будет совсем другой набор команд и микросервисов. Вот детали архитектуры, окружающей ваше приложение и порядок взаимодействия с ней — это и есть логика этого уровня.

Техника безопасности

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

  2. Частный случай. Я описал именно подход нашей команды: мы его собрали из кусочков разных идей и адаптировали под наши домены, наши процессы разработки, тестирования и рефакторинга. И тестирование тут — самое главное. Мне не известны надежные свидетельства и того, что наш подход достоверно делает разработку проще, дешевле или приятнее. Нам кажется, что оно так, но, когда кажется, креститься надо. Если вам этот подход кажется избыточно сложным — это нормально, можете, выбросить оттуда все, что вам не нужно.

  3. Я не упомянул Х. Если вы не увидели в описании свой любимый подход: реактивное программирование или, не знаю, Event‑sourcing, то это не значит, что эти идеи плохие. Просто я не могу описать в рамках одной статьи вообще все, поэтому я расписал только то, что посчитал самым важным.


Меня зовут Саша Раковский. Работаю техлидом в расчетном центре одного из крупнейших банков РФ, где ежедневно проводятся миллионы платежей, а ошибка может стоить банку очень дорого. Законченный фанат экстремального программирования, а значит и DDD, TDD, и вот этого всего. Штуки редкие, крутые, так мало кто умеет, для этого я здесь — делюсь опытом. Если стало интересно, добро пожаловать в мой блог.

Комментарии (28)


  1. SolidSnack
    29.07.2025 00:43

    Если честно в статье постоянно об что-то спотыкаешься (как будто вы сами не до конца разобрались) например:

    Из функционального программирования я бы порекомендовал взять уважение к неизменяемым структурам данных и избегание сайд-эффектов, где это возможно (например, CQS).

    Если открыть вашу ссылку на вики, то там видно что cqs это больше про ООП

    Ну и блок схемы ваши конечно прекрасны


  1. onets
    29.07.2025 00:43

    @RakovskyAlexander у вас тоже спрошу - вот есть у вас заказ, там 100500 полей (от номера и даты до суммы, тегов, заметок и прочего). Когда создаете сущность - как вы передаете все эти 100500 полей из ДТО, которое пришло из контроллера (адаптера)? Еще одно ДТО на уровне Domain или в обшей DLL и маппинг между ними? Все 100500 полей пихаете в конструктор? Какой либо еще вариант?


    1. SolidSnack
      29.07.2025 00:43

      Можно попробую ответить?) Вы когда результат с базы получаете можете его весь протолкнуть под типом "результат получения заказа" в конструктор программного объекта и обработать.


      1. onets
        29.07.2025 00:43

        Да, мы уже общались в коментах по другой статьей )) - у вас table module.

        И дружеское напоминание - речь идет не про конструирование сущности из БД, а про use case "создание заказа" или "обновление заказа". Когда DTO приходит извне.


        1. SolidSnack
          29.07.2025 00:43

          А какая разница? У вас так же данные приходят в удобном для вас виде и вы их обрабатываете, создаёте тип "запрос от клиента" и от него отталкиваетесь. Ну и собственно ваше DTO приходящие из вне уже какой-то тип который можно положить в какой-то конструктор.


          1. onets
            29.07.2025 00:43

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


    1. TsarS
      29.07.2025 00:43

      Группировать заказы можно же? Отдельным VO. Типа VO Address для 5 полей с адресом, VO tags для тегов и т.д. А в конструктор сущности передаются уже Address address, array tags и т.д. А их можно и отдельными DTO. Или я чего то не понял, влез не туда и это вопрос с подковыркой?


      1. ValeriyPus
        29.07.2025 00:43

        Скорее ответ:

        Агрегатом из доменных сущностей.

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

        Если обойтись без нарушения SOLID:

        AutoMapper поддерживает вложенные обьекты,

        FluentValidatior поддерживает вложенные обьекты,

        EF поддерживает вложенные обьекты.

        Возможно несколько ситуаций:

        1) Агрегат должен иметь связанный обьект, но его нет - обрабатываем в useCase (специфичный агрегат)

        2) Агрегат имеет связанный обьект - валидируем.

        3) В валидаторе - если связанный обьект есть - валидируем.

        Все, не надо городить 100 классов агрегатов (100 классов агрегатов - 100 проверок инвариантности). (да, я видел такое, и UseCase-ы в стиле UpdateFieldA(id,FieldA)) Обьясняют необходимостью инвариантности (BestPractice) - надо говорить что нужна только обеспечить валидность данных в хранилище.

        Но это с позиции CRM, где гриды и карточки обьектов первичны.

        Огромный плюс - 90% кода можно генерировать.

        Богатая доменная модель:

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


        1. onets
          29.07.2025 00:43

          Вот, много текста и там ниже еще коммент ваш, давайте постепенно разберемся.

          Значит, первое - я имел ввиду не про конструирование сущности из БД, а про use case "создание заказа" или "обновление заказа" или "отмена заказа".

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

          Далее, да вот я вычитал BestPractice - что сущность должна сама проверять свое состояние. То есть это свойства закрытые от изменений из вне (public get + private set).

          Таким образом, чтобы в use case "создание заказа" мне создать заказ из DTO, где куча полей - все эти поля нужно добавить в конструктор. Аналогично, чтобы в use case "обновление заказа" изменить поля - надо нагенерить кучу методов UpdateXXX(). И да, некоторые из них (update заметки) будут просто проверять, например, длину значения (не более 255 символов), а некоторые (update статус) - будут запускать сложные процессы. Пожалуй даже вместо UpdateStatus лучше сделать методы Approve() или Cancel().

          И вот мне эта тема с кучей параметров в конструкторе и кучей методов UpdateXXX не нравится.

          Далее из вашего комента ниже

          MediatR-style, для каждого useCase делаем команду и ответ.

          Команды уже с доменной сущностью (это уже Application)

          Это не решает проблему. Просто перекладываем из одно места в другое. Там все равно будет та же сущность с кучей параметров в конструкторе и кучей методов UpdateXXX. И все еще как-то надо подружить DTO и сущность.

          AutoMapper поддерживает вложенные обьекты

          Тут не совсем понятно - вложенные - это вложенные (они могут приватными и публичными) или приватные? Скорее всего приватные. Мне надо время, чтобы понять что вы написали. Отвечу чуть позже. Видимо у вас свойства сущности все же приватные, но вы в обход этого мапите их с использованием технического хака.


          1. ValeriyPus
            29.07.2025 00:43

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

            Если по неведомой причине смотреть на 2-3 строчки вверх в стектрейске ошибки лень -

            Делайте кодогенерацию, инжектите свой сервис валидации, в set запускайте валидацию агрегата.

            2) Богатая доменная модель нарушает SOLID, и доменный агрегат с 7 различными валидациями по статусу - мифический.

            https://habr.com/ru/articles/931866/comments/#comment_28631840

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

            3) Сделайте единый веб-маппинг. 1 доменный обьект - 1 WebDTO

            Откуда у вас появятся DTO со ста полями? Для такого должна быть доменная сущность со ста полями.

            Если будете на каждый UseCase писать свое DTO для каждого агрегата -можете устать


          1. ValeriyPus
            29.07.2025 00:43

            Вот простейший пример

            Данные для сущности

            namespace Aggregate.Domain
            {
                internal class DomainData 
                {
                    public int Id { get; set; }
            
                    public int Count { get; set; }
                    public DomainObject Build()
                    {
                        var res = new DomainObject
                        {
                            Id = Id,
                            Count = Count
                        };
            
                        res.Seal();
            
                        return res;
                    }
                }
            }
            

            Базовый класс для доменных сущностей

            namespace Aggregate.Domain
            {
                abstract class DomainEntity 
                {
                    protected bool IsSealed = false;
                    public void Seal()
                    {
                        IsSealed = true;
                    }
                }
            }
            

            И богатая доменная модель

            namespace Aggregate.Domain
            {
                internal class DomainObject : DomainEntity
                {
                    public int Id { get; set; }
            
                    protected int _count;
                    public int Count
                    {
                        get
                        {
                            return _count;
                        }
                        set
                        {
                            if (IsSealed)
                            {
                                //Проверяем
                                if (value < 0)
                                    throw new Exception("Count Должен быть больше нуля");
                            }
                            else
                                _count = value;
                                
                        }
                    }
                }
            }
            

            Пример использования:

            var data = new DomainData()
            {
                Id = 1,
                Count = 100
            };
            
            var item = data.Build();
            
            var data2 = new DomainData()
            {
                Id = 2,
                Count = -1
            };
            
            var item2 =  data2.Build();

            Аггрегат 1 создается, Агрегат 2 нет.

            В Build вызываем AutoMapper, и добавляем в конфигурацию

            https://stackoverflow.com/questions/6882826/how-to-make-automapper-call-a-method-after-mapping-a-viewmodel

            .AfterMap((c,s) => s.Seal());

            Все, агрегаты прямо богатых доменных моделей мапятся автоматом.

            Если пришел невалидный агрегат - получим ошибку при маппинге.

            Если вам нужно получить еще неизменяемую сущность - DomainDbData

            добавляете маппинги.

            Если заморочится - можно просто добавить запуск FluentValidator-а в setter-ах.

            Ну и весь этот бойлерплет генерировать )


            1. ValeriyPus
              29.07.2025 00:43

              то же самое, что и с Seal, можно сделать и с Lock (просто отключить сеттеры).

              Если не лень делать лишние обьекты можно еще

              добавить 3 обьекта DomainEntity - данные, DomainEntitySealed - валидный агрегат, DomainEntityLocked - Данные с БД, не изменяемые, только в валидный агрегат, и IDomainEntityLocked - интерфейс с {get;}.

              Все, агрегаты какой угодно сложности с инвариантностью готовы (спасибо AutoMapper).

              Данные в закрытые свойства через конструктор, возвращать из сервиса интерфейс. Все равно DomainEntityLocked будут только в БД, можно сделать internal.

              Валидацию агрегата можно делать FluentValidator-ом

              Меняем поле в set, вызываем Validate(this).

              Все рекурсивно валидируется (если есть в агрегате).

              Можно валидировать и одну доменную сущность, и какой угодно агрегат.


          1. michael_v89
            29.07.2025 00:43

            чтобы в use case "создание заказа" мне создать заказ из DTO, где куча полей - все эти поля нужно добавить в конструктор

            Не нужно. Вам же уже сказали выше - в этом случае в конструктор надо передавать DTO со всеми этими полями.

            Аналогично, чтобы в use case "обновление заказа" изменить поля - надо нагенерить кучу методов UpdateXXX().

            Аналогично, не надо. Если хотите делать логику в сущности, то в сущности должен быть метод update, который принимает DTO с полями из формы обновления заказа в интерфейсе и содержит логику обновления.

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


      1. SolidSnack
        29.07.2025 00:43

        После попадания ответа в конструктор вы можете делать что угодно - создавать объекты (фабрика) или создать 1 объект на весь пул данных. Автор изначального комментария актив рекордом смотрит, наверное


        1. ValeriyPus
          29.07.2025 00:43

          Вы устанете создавать 100 конструкторов, в 100 агрегатах )

          Или делать сто .toDomain() .fromDomain() как Автор.

          И делать там проверки и фабрики.

          Если речь про веб - у вас могут быть очень сложные агрегаты, деревья из 5 типов обьектов и проч. (И деревья из трех типов обьектов)

          Уже 2 разных агрегата с разной валидацией, или 2 разных конструктора и 2 логики валидации(!). Сложной валидации. (Потому что у одного агрегата проверяем привязанные обьекты на 3 уровне дерева, а другого - нет).

          Но это с позиции CRM, где гриды и карточки обьектов первичны.

          Возможно, где-то есть use-case-ы где надо прямо валидировать в момент изменения.

          Например, Orleans, где нет хранилища, и нельзя все проверять там.

          Но из-за непонимания DDD/CleanArchitecture вы скорее всего услышите про Инвариантность Обязательно в get/set, так написано! Rich Domain Model!

          Ну и из FP - да чистые обьекты и функции в Domain/Application! Без инфраструктуры!


        1. onets
          29.07.2025 00:43

          Ну нет же, у меня приходит CreateOrderDTO из контроллера, в БД еще ничего нет. Мне надо валидировать DTO, создать сущность, запустить некоторые сложные процессы (путем отправки события "создан заказ") и сохранить это в БД. И в данный момент проблема с "создать сущность" из этого DTO, так как у сущности все свойства имеют private set.


      1. onets
        29.07.2025 00:43

        Эмм, технически можно, но по логике - нет потребности. Просто есть use case "создание заказа" или "обновление заказа" или "отмена заказа".

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


        1. ValeriyPus
          29.07.2025 00:43

          (del)


    1. RakovskyAlexander Автор
      29.07.2025 00:43

      Привет!

      В нашей практике, маппинг дто на доменные объекты происходит в простом случае в коде дто. Мы просто делаем метод что-то вроде dto.toDomain(), внутри которого вызываются конструкторы или фабрики домена, и обратный статический Dto.from(). Ну только имена поприятнее.

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

      Так вот, в описанном подходе, если есть какая-то валидация доменного объекта при создании (например, в фабрике), то, хоть логика этой валидации и лежит в домене, но тесты, как будто, приходится писать либо в адаптере, либо отдельно на фабрику. Либо, третий вариант: использовать, как вы сказали, промежуточный объект, который, в отличие от модели, может быть в невалидном состоянии - этакий builder из gof. Последнее решение рекомендует дядя Боб в своей Чистой Архитектуре.

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


      1. onets
        29.07.2025 00:43

        Сразу не увидел этот коммент.

        В нашей практике, маппинг дто на доменные объекты происходит в простом случае в коде дто. Мы просто делаем метод что-то вроде dto.toDomain(), внутри которого вызываются конструкторы или фабрики домена, и обратный статический Dto.from()

        Это все еще не раскрывает решения проблемы маппинга. Свойства у сущности имеют private set? Как тогда маппинг, если они приватные?


        1. RakovskyAlexander Автор
          29.07.2025 00:43

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

          Сеттеры на доменных моделях, как правило, отсутствуют вовсе. Геттеры только по необходимости.


          1. onets
            29.07.2025 00:43

            Либо конструктор, либо фабрика, либо фабричный метод

            К сожалению я все еще не вижу решения - как это на самом деле работает.

            Может давайте псевдо кодом?

            public class Order
            {
              string Number;
              DateTime Date;
              // еще 100 500 полей
            
              public Order(/* 100 500 полей*/)
              {
            
              }
            
              // 100 500 методов
              public void UpdateXXX()
              {
                
              }
            }
            public class CreateOrderDTO
            {
              public string Number;
              public DateTime Date;
              // еще 100 500 полей
            }

            Допустим будет фабрика

            public class OrderFactory
            {
              public Order Create(CreateOrderDTO dto)
              {
                // Валидация DTO (поля публичные - проблем нет)
            
                // Вот тут у вас что?
                // Так?
                return new Order(dto.Number, dto.Date, /* остальные 100 500 полей из DTO*/)
              }
            }


  1. michael_v89
    29.07.2025 00:43

    Когда вся логика лежит в сервисах, а не в доменных объектах, то это все еще работает, но уже не особо что-то моделирует.

    Моделирует. То, что и должна - бизнес-требования.

    // Если заказ успешно оплачен, перевести заказ в статус "Оплачено"
    // и отправить письмо пользователю с информацией о заказе.
    
    class OrderService {
      public function pay() {
        ...
        
        $paymentResult = $this->sendPayment($order, $paymentDetails);
        
        if ($paymentResult === PaymentResult::Success) {
          $this->moveOrderToStatus(OrderStatus::PAID);      
          $this->sendSuccessEmail(order);
        }
      }
    
      private function moveOrderToStatus(OrderStatus $status) {
          $order->setStatus($status);
          $this->entityManager->save($order);
      }
    }
    

    Как вы будете делать оплату и отправку email из сущности, непонятно. Учтите, что отправку email надо делать после сохранения сущности.

    Обратите внимание, что в бизнес-требованиях написано "перевести заказ в статус N", а не "перевести это в статус N". Поэтому код $order->setStatus(N) более правильно моделирует бизнес-требования, чем $this->status = N.


  1. ValeriyPus
    29.07.2025 00:43

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

    Богатая модель - антипаттерн SOLID.

    Попробуйте как-нибудь сделать обьект с 7 различными проверками в зависимости от статуса, например.

    А теперь добавим функции сохранения, расчета...

    В том же EF инжектить сервисы в обьекты - проблематично.

    @onets

    Когда создаете сущность - как вы передаете все эти 100500 полей из ДТО, которое пришло из контроллера (адаптера)? 

    MediatR-style, для каждого useCase делаем команду и ответ.

    Команды уже с доменной сущностью (это уже Application).

    Запросы и ответы - и есть отвязка Application от Infrastructure.

    Можно сразу переиспользовать все UseCase-ы, и вызывать их откуда угодно.

    Маппинг WebDTO-DomainEntity в Infrastructure.


  1. Lewigh
    29.07.2025 00:43

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

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

    Сложность реализации тривиальных вещей может быть избыточна.

    Ну во-первых, ФП ФП рознь. Есть экстремальные вещи вроде чистого Haskell подобного ФП а есть умеренные вроде Clojure.
    Во-вторых, в ООП такая же история. Сколько часто нужно нагородить классов, отнаследоваться, попереопределять, открыть закрыть чтобы написать тривиальную вещь на ООП языке.

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

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

    У ООП свои очень серьезные преимущества, которые я рекомендую не игнорировать. Повторюсь, эти преимущества:

    • высокий cohesion за счет хранения данных и поведения в одном месте

    • хорошие инструменты инкапсуляции модели, защищающие вашу модель от некорректного состояния

    • полиморфизм, позволяющие устранять дублирование там, где в ином случае вы были бы обречены.

    Может быть в теории. На практике это не очень работает и почти все пишут опираясь на анемичную модель.

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

    • Анемичная доменная модель

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

      В целом, анемичная доменная модель - это Transaction Script с издержками доменной модели, худшее из двух подходов.

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


  1. Kerman
    29.07.2025 00:43

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

    Да ладно! Вы решили, что ORM - это не религия, а всего лишь инструмент управления сложностью, он не всегда подходит и надо им пользоваться там, где его применение оправдано?


  1. powerman
    29.07.2025 00:43

    Удивительное дело, всегда когда агитируют за использование тактики DDD почему-то в статьях и примерах кода забывают упомянуть вопросы работы с транзакциями, выбора границ агрегатов и подходы к обеспечиванию консистентности между разными сервисами и даже разными агрегатами в рамках одного сервиса. А это очень важный (на мой взгляд даже ключевой) момент в тактической части DDD: Пиррова победа Domain-Driven Design.

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

    С этим помогает отказ от религиозного следования догмам. Тактическое DDD - это очень дорогое удовольствие, и нет никакого смысла платить эту цену в 95% сервисов где просто нет настолько сложной бизнес-логики. Никакое "единообразие архитектуры проекта" не стоит этой цены! Поэтому стоит использовать в абсолютном большинстве сервисов Transaction Script (разумеется, плюс гексагональную), и прибегать к тактическому DDD только в тех сервисах, где сложность бизнес-логики действительно на порядок выше средней.


  1. olku
    29.07.2025 00:43

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