Перейти к основному содержимому

6.11. Паттерны доменного моделирования

Разработчику Архитектору Аналитику

Паттерны доменного моделирования

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

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

Истоки и контекст применения

Идея паттернов доменного моделирования получила широкое распространение благодаря работам Мартина Фаулера и Эрика Эванса. В книге «Patterns of Enterprise Application Architecture» Фаулер описал набор архитектурных решений, ориентированных на организацию бизнес-логики. Эванс в своей работе «Domain-Driven Design» предложил более глубокий подход к взаимодействию с предметной областью, введя такие понятия, как единый язык (Ubiquitous Language), агрегаты, сущности и объекты-значения.

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

Основные паттерны организации бизнес-логики

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

Транзакционный скрипт

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

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

Таблица активных записей

Таблица активных записей (Active Record) объединяет данные и поведение в одном объекте, который одновременно представляет строку таблицы в базе данных и содержит методы для работы с этой строкой. Объект знает, как сохранить себя, как обновить свои данные, как удалить себя из базы. Часто такие объекты также содержат методы для выполнения запросов к таблице.

Этот паттерн популярен в фреймворках, ориентированных на быструю разработку, таких как Ruby on Rails или Laravel. Он упрощает создание CRUD-операций и позволяет быстро строить прототипы. Однако он смешивает обязанности доступа к данным и бизнес-логики, что затрудняет разделение ответственностей. При усложнении логики объекты становятся перегруженными, а зависимость от структуры базы данных делает систему хрупкой к изменениям схемы.

Доменный объект

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

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

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

Сервис предметной области

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

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

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

Связь с Domain-Driven Design

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

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

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

Единый язык (Ubiquitous Language) — это ещё один ключевой элемент DDD, который напрямую связан с паттернами моделирования. Все участники проекта — разработчики, аналитики, бизнес-эксперты — используют одни и те же термины для описания предметной области. Код становится документацией, а названия классов, методов и переменных отражают реальные понятия бизнеса. Паттерны доменного моделирования обеспечивают структуру, в которой этот язык может быть реализован.

Примеры применения паттернов в реальных системах

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

Система управления складом

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

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

Финансовая платформа

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

Более корректным решением станет выделение доменных объектов «Счёт», «Валюта», «Комиссия» и сервиса «ПереводСредств». Сервис будет вызывать методы объектов, проверяя условия и применяя правила. Все изменения происходят в рамках одной транзакции, гарантируя целостность данных. Такая структура делает логику прозрачной и позволяет легко тестировать различные сценарии.

Платформа электронной коммерции

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

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

Сравнение паттернов по ключевым характеристикам

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

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

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

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

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

Интеграция с современными фреймворками и технологиями

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

В экосистеме .NET доменные объекты легко реализуются с помощью классов C#, а сервисы — через интерфейсы и внедрение зависимостей. Entity Framework Core может использоваться как ORM-слой, но важно отделить доменные объекты от сущностей EF, чтобы избежать зависимости от инфраструктуры.

В JavaScript-приложениях доменные объекты могут быть представлены как обычные классы или функции, возвращающие объекты с методами. Библиотеки вроде MobX или Redux помогают управлять состоянием, но бизнес-логика должна оставаться в доменных объектах, а не в редьюсерах или наблюдаемых переменных.

В Python-проектах доменные объекты часто реализуются с помощью dataclasses или Pydantic-моделей, но важно помнить, что валидация и преобразование данных — это не то же самое, что бизнес-правила. Логика должна быть вынесена в методы объектов, а не в декораторы или внешние функции.

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