Анемичная модель предметной области — не анти-шаблон, а архитектура по принципам SOLID. Анемия

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

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

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


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


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


Я убежден, что одним из таких незаслуженно отвергаемых анти-шаблонов является Анемичная модель предметной области (АМПО, Anaemic Domain Model), описанная Мартином Фаулером и Эриком Эвансом . Оба автора описывают этот шаблон как неспособность смоделировать предметную область в объектно-ориентированном стиле, из-за чего бизнес-логика описывается в процедурном стиле. Такой подход противопоставляется Богатой модели предметной области (БМПО, Rich Domain Model) , - в ней классы, представляющие сущности предметной области, содержат в себе и данные, и всю бизнес-логику. Да, анемичная модель может быть неудачным выбором для некоторых систем, но совершенно не факт, что то же самое справедливо для любых систем. В этой статье я рассматриваю аргументы, выдвигаемые против анемичной модели, и обосновываю, почему в ряде сценариев АМПО выглядит разумным выбором с точки зрения соответствия принципам SOLID, сформулированным Робертом Мартином (, ), - принципам, в которых заключены рекомендации по достижению баланса между простотой, масштабируемостью и надежностью при разработке ПО. Решая гипотетическую проблему и сравнивая анемичную и богатую модели, я намерен показать, что АМПО лучше соответствует приципам SOLID. Тем самым я хочу оспорить категоричное мнение об этом подходе, навязанное авторитетами, и показать, что использование АМПО - на самом деле, годное архитектурное решение.

Почему анемичную модель предметной области считают анти-шаблоном?

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


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


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

Простой пример

Давайте представим серверную часть интернет-магазина, где клиент может как покупать товары, так и выставлять на продажу товары для других клиентов со всего земного шара. Приобретение товара приводит к уменьшению средств на счету покупателя. Подумаем, как можно реализовать процесс размещения клиентом заказа на приобретение товара. Согласно требованиям, клиент может разместить заказ, если у него а) достаточно средств на счету, и б) товар доступен в регионе клиента. При использовании БМПО, класс Customer будет описывать сущность «Клиент»; он будет включать все свойства клиента и такие методы как PurchaseItem(Item item) (Купить товар). Аналогично, классы Item и Order представляют модели предметной области, описывающие сущности Товар и Заказ, соответственно. Реализация класса Customer (на псевдо-C#) может быть примерно такой:


/*** КОД С ИСПОЛЬЗОВАНИЕ БМПО ***/ class Customer: DomainEntity // Базовый класс, предоставляющий CRUD-операции { // Опускаем объявление закрытых членов класса public bool IsItemPurchasable(Item item) { bool shippable = item.ShipsToRegion(this.Region); return this.Funds >= item.Cost && shippable; } public void PurchaseItem(Item item) { if (IsItemPurchasable(item)) { Order order = new Order(this, item); order.Update(); this.Funds -= item.Cost; this.Update(); } } } /*** КОНЕЦ КОДА С ИСПОЛЬЗОВАНИЕ БМПО ***/

Сущности предметной области реализуются с использованием шаблона Active Record , в котором используются методы Create/Read/Update/Delete (реализованные на уровне фреймворка или базового класса), позволяющие изменять записи в слое хранения данных (например, в базе данных). Предполагается, что метод PurchaseItem вызывается в рамках транзакции, совершаемой над хранилищем данных и управляемой извне (например, она может открываться в обработчике HTTP-запроса, который извлекает информацию о клиенте и товаре непосредственно из переданных в запросе параметров). Получается, что в нашей БМПО роль сущности «Клиент» состоит 1) в представлении модели данных, 2) реализации бизнес-правил, 3) создании сущности «Заказ» для совершения покупки и 4) взаимодействии со слоем хранения данных посредством методов, определенных для Active Record. Воистину, «богатству» такой модели позавидовал бы царь Крез, а мы ведь рассматривали довольно простой вариант использования.


Следующий пример иллюстрирует, как та же логика могла бы быть выражена средствами АМПО, в тех же условиях:


/*** КОД С ИСПОЛЬЗОВАНИЕМ АМПО ***/ class Customer { /* Some public properties */ } class Item { /* Some public properties */ } class IsItemPurchasableService: IIsItemPurchasableService { IItemShippingRegionService shipsToRegionService; public bool IsItemPurchasable(Customer customer, Item item) { bool shippable = shipsToRegionService.ShipsToRegion(item); return customer.Funds >= item.Cost && shippable; } } class PurchaseService: IPurchaseService { ICustomerRepository customers; IOrderFactory orderFactory; IOrderRepository orders; IIsItemPurchasableService isItemPurchasableService; // Конкретные экземпляры инициализируются в конструкторе public void PurchaseItem(Customer customer, Item item) { if (isItemPurchasableService.IsItemPurchasable(customer, item)) { Order order = orderFactory.CreateOrder(customer, item); orders.Insert(order); customer.Balance -= item.Cost; customers.Update(customer); } } } /*** КОНЕЦ КОДА С ИСПОЛЬЗОВАНИЕМ АМПО ***/

Сравнение примеров реализации с точки зрения соответствия принципам SOLID

На первый взгляд, АМПО явно проигрывает БМПО. В ее реализации использовано больше классов, а логика размазана по двум доменным службам (IPurchaseService и IItemPurchasableService ) и ряду служб приложения (IOrderFactory , ICustomerRepository и IOrderRepository ), вместо того чтобы располагаться в пределах модели предметной области. Классы предметной области теперь не содержат никакого поведения, а всего лишь хранят данные и допускают изменение своего состояния вне рамок наложенных ограничений (и - о ужас! - утрачивают способность обеспечивать собственные инварианты). Учитывая все эти явные недостатки, как вообще можно рассматривать такую модель как конкурента куда более объектно-ориентированной БМПО?


Причины, по которым АМПО является превосходным выбором для данного сценария, проистекают из рассмотрения принципов SOLID и их наложения на обе рассматриваемые архитектуры . «S» означает «Принцип единственной ответственности» (Single Responsibility Pronciple, ), который гласит, что класс должен делать только что-то одно - но делать это хорошо. В частности, класс должен реализовывать лишь одну абстракцию. «O» - «Принцип открытости/закрытости» (Open/Closed Principle, ), постулат о том, что класс должен быть «открытым для расширения, но закрытым для изменения». Это означает, что при разработке класса надо максимально стремиться к тому, чтобы реализацию не пришлось изменять в будущем, тем самым сводя к минимуму последствия вносимых изменений.


Казалось бы, класс Customer в БМПО реализует единственную абстракцию «Клиент», но на самом деле этот класс отвечает за множество вещей. Этот класс моделирует и данные, и логику в рамках одной и той же абстракции, несмотря на то, что бизнес-логика имеет обыкновение меняться куда чаще, чем структура данных. Этот же класс создает и инициализирует сущности «Заказ» в момент совершения покупки, и даже содержит логику, определяющую, может ли клиент совершить покупку. А предоставляя базовые CRUD-операции, определенные в базовом классе, сущность предметной области «Клиент» оказывается еще и связанной с той моделью хранилища данных, которая поддерживается базовым классом. Стоило нам перечислить все эти обязанности, как стало очевидным, что сущность Customer в БМПО являет собой пример слабого разделения ответственности.


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

Сравнение гибкости решений на базе богатой и анемичной моделей предметной области

Рассмотрим примеры сценариев, при которых нам пришлось бы изменять класс Customer в БМПО.

  • Необходимо добавить новое поле (или изменить тип данных существующего).
  • В конструктор класса Order необходимо передать дополнительный параметр.
  • Бизнес-логика, относящаяся к покупке товара, усложнилась.
  • Возникла необходимость сохранения данных в альтернативное хранилище, которое не поддерживается нашим гипотетическим базовым классом DomainEntity.

Теперь рассмотрим сценарии, в которых нам необходимо изменить типы, описанные в АМПО. Классы бизнес-сущностей, чье предназначение состоит в моделировании предметной области, подлежат изменению тогда и только тогда, когда изменяются требования к составу данных. В случае усложнения правил, по которым определяется возможность приобретения того или иного товара (например, для товара указывается минимально допустимый «рейтинг доверия» клиента, которому этот товар может быть продан), изменению подлежит только реализация IIsItemPurchasableService , в то время как при использовании БМПО нам пришлось бы соответствующим образом изменять класс Customer . Если меняются требования к хранилищу данных - в АМПО задача решается путем передачи в PurchaseService из вышестоящего класса служб приложения новой реализации существующего интерфейса репозитория , , не требуя модификации существующего кода; в БМПО так легко не отделаться, модификация базового класса затронет все классы бизнес-сущностей, унаследованных от него. В случае, когда для создания экземпляра класса Order необходимо передать дополнительный параметр, реализация IOrderFactory может оказаться в состоянии обеспечить это изменение, не оказывая влияния на PurchaseService . В анемичной модели у каждого класса единственная ответственность, и вносить изменения в класс придется только при изменении соответствующего требования в предметной области (или связанной инфраструктуре).


А сейчас представим, что для реализации нового бизнес-требования мы должны обеспечить возможность возврата средств, если клиент не удовлетворен покупкой. В богатой модели это можно было бы реализовать путем добавления метода RefundItem к сущности «Клиент», аргументируя это тем, что вся логика, относящаяся к клиенту, оказывается заключенной в сущности Customer . Однако процедура возврата денежных средств сильно отличается от процедуры совершения покупки, ответственность за которую ранее была возложена на класс Customer , и в результате мы получаем еще большее смешение ответственностей в пределах одного типа. Выходит, в классах богатой модели могут накапливаться слабо связанные элементы бизнес-логики, повышая сложность их структуры. В анемичной модели механизм возврата денежных средств можно реализовать путем создания нового класса RefundService , который будет реализовывать только логику, непосредственно относящуюся к возвратам. Этот класс может зависеть от нескольких других абстракций (т.е. интерфейсов других доменных и инфраструктурных служб), необходимых для выполнения им своих обязанностей. Обращение к методам класса RefundService может происходить из вышележащих уровней (в ответ на запрос об осуществлении возврата денежных средств), и выходит, что реализацию нового сценария удалось выполнить безо всякого влияния на ранее разработанную функциональность.


В рассмотренном примере проблема закрепления за одним классом не связанных между собой ответственностей, с которой мы столкнулись в БМПО, эффективно решается в анемичной модели при помощи букв I и D из аббревиатуры SOLID. Это, я напомню, «Принцип разделения интерфейса» (Interface Segregation Principle, ) и «Принцип инверсии зависимостей» (Dependency Inversion Principle, ). Они утверждают, что интерфейсы должны представлять собой наборы сильно сцепленных методов, и что интерфейсы должны использоваться для соединения частей системы воедино (в случае АМПО - соединение служб доменного слоя между собой). Следование принципу разделения интерфейса, как правило, дает в результате небольшие, узкоспециализированные интерфейсы - такие как IItemShippingRegionService и IIsItemPurchasableService из нашего примера, или интерфейс абстрактного репозитория. Принцип инверсии зависимостей заставляет нас опираться на эти интерфейсы, чтобы одна служба не зависела от деталей реализации другой.

Анемичная модель предметной области лучше поддерживает автоматизированное тестирование

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


Согласно предъявленным требованиям, товар считается доступным для покупки, если у клиента достаточно средств на счету, и он находится в регионе, куда этот товар может быть доставлен. Положим, мы пишем тест, проверяющий, что если у клиента достаточно средств на счету, но он не находится в регионе, куда осуществляется доставка данного товара, то этот товар недоступен для покупки. В БМПО такой тест, вероятно, включал бы создание экземпляров Клиент (Customer ) и Товар (Item ), настройку Клиента таким образом, чтобы средства на его счету превышали стоимость Товара, и чтобы его регион не входил в перечень регионов, куда этот товар доставляется. После чего мы должны были бы убедиться, что customer.IsItemPurchasable(item) возвращает значение false . Однако метод IsItemPurchasable зависит от деталей реализации метода ShipsToRegion класса Item . Изменение бизнес-логики, относящейся к товару, приведет к изменению результатов этого теста. Такой эффект нежелателен, так как данный тест должен проверять исключительно логику, заключенную в классе Customer , а логика метода ShipsToRegion , заключенная в сущности «Товар», должна покрываться отдельным тестом. Поскольку бизнес-логика заключена в сущностях, описывающих предметную область и предоставляющих открытый интерфейс для доступа к заключенной в них логике, классы оказываются сильно связанными, что приводит к лавинообразному эффекту при внесении изменений, из-за чего автоматизированные тесты становятся хрупкими.


С другой стороны, в АМПО логика метода IsItemPurchasable вынесена в отдельную специализированную службу, которая зависит от абстрактных интерфейсов (метод IItemShippingRegionService.ShipsToRegion ). Для рассматриваемого теста мы можем попросту создать заглушку для IItemShippingRegionService , в которой будет реализован метод ShipsToRegion , всегда возвращающий false . Разделив бизнес-логику по изолированным модулям, мы защитили каждую часть от изменений деталей реализации в других частях. На практике это означает, что небольшое изменение логики скорее всего приведет к «падению» лишь тех тестов, которые непосредственно проверяют поведение того кода, в который были внесены изменения, что можно использовать для проверки правильности нашего представления об изменяемом коде.

Рефакторинг БМПО с целью соблюдения принципов SOLID приводит к «анемии» модели

Сторонники архитектуры, использующей БМПО, могут возразить, что описанный гипотетический пример не соответствует «истинной» богатой модели. Они скажут, что в правильно реализованной богатой модели нельзя смешивать сущности предметной области с задачами по их записи в хранилище - вместо этого предпочтительнее использовать объекты передачи данных (DTO, Data Transfer Object, , ), посредством которых происходит обмен со слоем хранения данных. Они разнесут в пух и прах идею прямого вызова конструктора класса Order непосредственно из логики класса Customer - разумеется, ни в одной вменяемой реализации сущности предметной области не будут вызывать конструктор напрямую, здравый смысл заставляет использовать фабрику ! Но по мне, это выглядит как попытка применять мощь принципов SOLID к инфраструктурным службам, при полном их игнорировании в приложении к модели предметной области. Если нашу гипотетическую БМПО рефакторить для соответствия принципам SOLID, будут выделены более мелкие сущности: из сущности Клиент могут быть выделены сущности «Покупка клиента» (CustomerPurchase ) и «Возврат ден.средств клиента» (CustomerRefund ). Но может статься, что и новые модели будут по-прежнему зависеть от элементарных бизнес-правил, изменяемых независимо друг от друга, а от них, в свою очередь, будут зависеть другие сущности. Во избежание дублирования логики и сильной связанности классов эти правила придется и дальше рефакторить, выделяя их в отдельные модули, доступ к которым осуществляется посредством интерфейсов. В итоге, богатая модель, отрефакторенная до полного соответствия принципам SOLID, стремится к состоянию анемичной модели!

Заключение

Исследовав реализацию простого примера, мы пришли к выводу, что анемичная модель предметной области ближе соответствует принципам SOLID, чем богатая модель. Мы увидели преимущества, которые дает соответствие принципам SOLID: слабую связанность и сильную сцепленность, повышающие гибкость архитектуры приложения. Свидетельством возросшей гибкости явилось улучшение тестируемости приложения из-за легкости реализации «заглушек» для зависимостей. Рассматривая пути достижения этих же качеств в рамках БМПО, мы обнаружили, что рефакторинг богатой модели закономерно приводит к ее «анемичности».


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

Ссылки

Развернуть

Evans, Eric. Domain-driven design: tackling complexity in the heart of software. Addison-Wesley Professional, 2004.


Martin, Robert C. The Principles of Object-Oriented Design. http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod , 2005.


Martin, Robert C. Design principles and design patterns. Object Mentor, 2000: 1-34.


Erich, Gamma, et al. Design patterns: elements of reusable object-oriented software. Addison Wesley Publishing Company, 1994.


Wolfgang, Pree. Design patterns for object-oriented software development. Addison-Wesley, 1994.


Rising, Linda. The patterns handbook: techniques, strategies, and applications. Vol. 13. Cambridge University Press, 1998.


Budgen, David. Software design. Pearson Education, 2003.


Scott, Michael L. Programming language pragmatics. Morgan Kaufmann, 2000.


Hevery, Miško. Writing Testable Code. http://googletesting.blogspot.co.uk/2008/08/by-miko-hevery-so-you-decided-to.html , Google Testing Blog, 2008.


Osherove, Roy. The Art of Unit Testing: With Examples in. Net. Manning Publications Co., 2009.


Martin, Robert C. Agile software development: principles, patterns, and practices. Prentice Hall PTR, 2003.


Martin, Robert C. SRP: The Single Responsibility Principle. , Object Mentor, 1996.


Martin, Robert C. The Open-Closed Principle. , Object Mentor, 1996.


Martin, Robert C. The Interface Segregation Principle. , Object Mentor, 1996.


Martin, Robert C. The Dependency Inversion Principle, Object Mentor, 1996.


Fowler, Martin. Patterns of enterprise application architecture. Addison-Wesley Longman Publishing Co., Inc., 2002.


Fowler, Martin. Data Transfer Object. http://martinfowler.com/eaaCatalog/dataTransferObject.html , Martin Fowler site, 2002.

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

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

Задача

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

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

Начнем с анемичной модели данных. У нас будет класс Account :

Public class Account { public int Id { get; set; } public bool IsApproved { get; set; } public DateTime? ActivationDate { get; set; } public List Orders { get; set; } }

И класс Order :

Public class Order { public int Id { get; set; } public int Price { get; set; } public Account Account { get; set; } public bool IsComplete { get; set; } }

Реализация

Каждый из сценариев работы довольно просто реализовать:

Сценарий №1. Активация пользователя

Account.ActivationDate = DateTime.Now; account.IsApproved = true;

Сценарий №2. Добавление заказа

Account.Orders.Add(order); order.Account = account;

Сценарий №3. Подсчёт общей суммы

Account.Orders .Where(order => order.IsComplete == false) .Sum(order => order.Price);

Главный вопрос: где будет располагаться этот код?

Решение №0

Есть самое простое и неправильное решение. Мы будем писать этот код прямо в обработчиках на aspx -страницах или WinForms:

Public partial class Default: Page { protected void Page_Load(object sender, EventArgs e) { // выборка объекта account AccountOrdersSumLabel.Text = account.Orders .Where(order => order.IsComplete == false) .Sum(order => order.Price); } protected void AddOrderButton_Click(object sender, EventArgs e) { // выборка объекта account account.Orders.Add(order); order.Account = account; // сохранение объекта account } }

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

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

Решение №1

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

Получаем:

Public static class AccountHelper { public static void Activate(Account account) { account.ActivationDate = DateTime.Now; account.IsApproved = true; } public static void AddOrder(Account account, Order order) { account.Orders.Add(order); order.Account = account; } public static int CalculateOrdersSum(Account account) { return account.Orders .Where(order => order.IsComplete == false) .Sum(order => order.Price); } }

Проблема классов с названием *Helper или *Manager в том, что они могут себе позволить делать всё, что угодно. Их абстрактные названия позволяют «помогать» классу Account делать абсолютно разные вещи. Такие классы со временем становятся .

У таких классов множество недостатков. Например, трудно тестировать код, который использует эти классы, потому что они статические. Они делают код сильно связаным, т.к. нарушают . Очень часто из одного Helper "а вызывают другие Helper "ы. В итоге, граф зависимостей напоминает паутину из связей.

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

Решение №2

Public interface IAccountService { void Activate(Account account); void AddOrder(Account account, Order order); int CalculateOrdersSum(Account account); } public class AccountService: IAccountService { public void Activate(Account account) { account.ActivationDate = DateTime.Now; account.IsApproved = true; } public void AddOrder(Account account, Order order) { account.Orders.Add(order); order.Account = account; } public int CalculateOrdersSum(Account account) { return account.Orders .Where(order => order.IsComplete == false) .Sum(order => order.Price); } }

Разобрались со связанностью и тестирование. Уже шаг вперёд. Но я вижу ещё две проблемы.

Функций типа AddOrder и CalculateOrdersSum будет довольно много. Через пол года разработки интерфейс IAccountService вырастит до 40-50 функций. «Загрязнение» интерфейса можно было бы пережить, если бы не вторая проблема.

В коде в любом месте можно в обход сервиса написать «свою активацию» пользователя. Например, взять объект Account из базы, выставить ему поле IsApproved в true и при этом забыть обновить поле ActivationDate . Тоже самое касается сценария добавления заказа. Можно вызвать функцию Add у свойства Orders где угодно и забыть выставить поле Account у добавляемого заказа. Это делает систему нестабильной. API приложения беззащитно перед пользователями системы. С таким подходом остается только надеятся, что программист найдёт нужную ему функцию в IAccountService , а не станет изобретать свой подход.

Решение №3

Поместим все эти функции в сам доменный объект Account . Обратите внимание на то, как изменились модификаторы доступа к полям объекта:

Public class Account { private readonly List orders; public Account() { orders = new List(); } public int Id { get; set; } public bool IsApproved { get; private set; } public DateTime? ActivationDate { get; private set; } public IEnumerable Orders { get { return orders; } } public int OrdersSum { get { return Orders .Where(order => order.IsComplete == false) .Sum(order => order.Price); } } public void Activate() { ActivationDate = DateTime.Now; IsApproved = true; } public void AddOrder(Order order) { orders.Add(order); order.Account = this; } }

Теперь домен нашего приложения даёт пользователю готовое API, которое не требудет ни Helper "ов, ни сервисов. К тому же мы уберегаем пользователя от ошибок. Он уже не сможет активировать Account выставив только IsApproved . Теперь функция Activate сама заполнит нужные поля.

Заключение

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

Мартином Фаулером в его классическом труде "Шаблоны корпоративных приложений" (Patterns of enterprise application architecture ) выделено несколько подходов к организации бизнес-логики.

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

    Выделяют два варианта данного подхода:

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

Шаблон Модель предметной области

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

Важным преимуществом так же является тот факт, что Модель предметной области позволяет повторно использовать бизнес-логику приложения. В отличие от шаблонов Транзакционный сценарий и Табличный модуль, бизнес-логика не привязана к запросам от слоя представления. Соответственно над ней можно стоить другие варианты слоя представления, например RESTful- или SOAP -сервисы.

К сожалению, за любое достоинство следует платить. В данном случае плата заключается в следующем. Во-первых, повышаются требования к квалификации разработчиков, проектирующих, реализующих и поддерживающих модель предметной области: необходимо уметь строить более сложные абстракции нежели при применении других решений. Во-вторых, повышаются требования к используемым технологиям. В частности нужно иметь довольно мощный слой доступа к данным, который реализовывал бы объектно-реляционное отображение. Реализация шаблона Модель предметной области как правило требует применение мощного ORM -фреймворка, такого как Hibernate , TopLink , EclipseLink или OpenJPA , что с одной стороны требует разработчиков более высокой квалификации, а с другой - снижает производительность разрабатываемой системы и повышает ее хрупкость: любым неосторожным действием, например изменением одной строчки в описании объектно-реляционного отображения можно .

Анатомия насыщенной и анемичной моделей

Чем анатомически отличаются два подхода к построению модели предметной области: насыщенный и анемичный? При насыщенном подходе бизнес-логика, т.е. поведение системы реализуется внутри объектов предметной области. Данный подход не исключает вынесение части поведения в служебные классы, такие как *Service , *Manager , *Helper , но это оправдано только, когда логика затрагивает несколько доменных объектов и не понятно к какому именно она относится. Примером может являться следующая небольшая часть модели предметной области "Телеком". Пользователю нужно подключить себе услугу. Услуга добавляется в рамках заказа. При этом необходимо проверить, что есть организация-провайдер, которая может предоставить услугу с заданным набором параметров. В случае насыщенной доменной модели логика добавления услуги инкапсулируется в методе addNewService(Service service) класса ServiceOrder :

private List < Service> services;

private void addService(Service service) {

if (services == null )

Services = new ArrayList < Service> () ;

Services.add (service) ;

public void addNewService(Service service) throws NotFoundValidProviderException {

ServiceProviderRepository providerRepo = ...;

if (providerRepo.hasAvailableProvider (service.getProp1 () , service.getProp2 () , ...) ) {

AddService(service) ;

else {

throw new NotFoundValidProviderException(service) ;


Анемичная модель предметной области устроена иначе. Классы объектов предметной области лишены поведения. Они имеют только конструкторы и методы доступа к данным. Единственное, что они реализуют - это отношения с другими объектами. Все поведение системы выносится в слой сервисов, реализованный поверх слоя модели предметной области. Продемонстрируем данный подход на том же самом примере, однако теперь класс ServiceOrder будет иметь только методы, осуществляющие доступ к данным. Определение того, имеется ли подходящий провайдер услуги и ее добавление в заказ будет осуществляться в ServiceOrderManager (согласитесь, что ServiceOrderService звучит как-то странно):

public class ServiceOrder {

private List < Service> services;

public List < Service> getServices() {

return services;

public void setServices(List < Service> services) {

this .services = services;


return providerRepo;

if (order.getServices () == null )

Order.setServices (new ArrayList < Services> () ) ;

Order.getServices () .add (service) ;

else {

Критика Анемичной модели предметной области

В сообществе разработчиков распространены два основных соображения против применения шаблона Анемичная модель предметной области:
  • При использовании анемичной модели предметной области мы отклоняемся от принципов ООП . Суть данного критического замечания сводится к тому, что т.к. объекты предметной области в случае анемичной доменной модели не имеют поведения, то мы вступаем в противоречие с базовой идеей ООП - иметь данные и методы их обработки в одном месте.
  • При вырождении модели предметной области в анемичную, теряются все преимущества, которые дает данный шаблон, при этом сохраняются его недостатки . В частности, применение данного шаблона по прежнему требует довольно мощного слоя доступа к данным, реализованного, например, с использованием громоздких ORM -фреймворков.
Давайте рассмотрим данные соображения подробнее. Анемичная модель предметной области противоречит принципам ООП . В каком-то смысле данное соображение справедливо, т.к. объекты предметной области не имеют поведения. Однако доступ ко внутреннему состоянию объектов можно инкапсулировать за методами данного объекта. Продемонстрируем данную идею на нашем примере с добавлением сервиса в заказа: в класс ServiceOrder добавляется метод addService() , при этом метод setServices() делаем приватным:

public class ServiceOrder {

private List < Service> services;

public List < Service> getServices() {

return services;

private void setServices(List < Service> services) {

this .services = services;

public void addService(Service service) {

if (services == null )

Services = new ArrayList < Service> () ;

Services.add (service) ;


Класс ServiceOrderManagerImpl будет осуществлять проверку возможности добавления заказа и вызывать метод addService() класса ServiceOrder :

public class ServiceOrderManagerImpl implements ServiceOrderManager {

private ServiceProviderRepository providerRepo;

public ServiceProviderRepository getProviderRepo() {

return providerRepo;

public void setProviderRepo(ServiceProviderRepository providerRepo) {

this .providerRepo = providerRepo;

public void addNewService(ServiceOrder order, Service service) throws ProviderNotFoundException {

if (providerRepo.hasAvailableProvider (service.getParam1 () , service.getParam2 () , ...) ) {

Order.addService (service) ;

else {

throw new ProviderNotFoundException(service) ;


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

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

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




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

public class Account {

private OverdraftLimitStrategy overdraftLimitStrategy;

public OverdraftLimitStrategy getOverdraftLimitStrategy() {

return overdraftLimitStrategy;


В некотором AccountService мы теперь можем использовать полиморфную реализацию стратегии расчета овердрафта:

public class AccountService {

public void withdraw(Account account, Double amount) {

if (account.getAmount () + account.getOverdraftLimitStrategy () .getLimit () > = amount) {

Account.setAmount (account.getAmount () - amount) ;


Теряются все преимущества, которые дает шаблон Модель предметной области . Как мы рассмотрели выше, преимущества, которые дает моделирование системы в ООП-стиле при использовании анемичной модели предметной области нам по прежнему доступны. Возможно, что получить данные преимущества можно только при грамотной проработке архитектуры системы, в частности - интерфейсов между слоями, но это - соответствующая плата за более простую модель. Способность объектов прозрачно сохраняться в долговременной памяти так же не теряется. Как раньше объект сохранялся соответствующей командой в репозитории (Repository ) или участке работы (Unit of Work ), так и продолжает сохранятся. Синхронизация изменений внутреннего состояния объекта с хранилищем данных не зависит от того, как именно выполняется данное изменение - методом внутри класса объекта или методом сервиса.

Причины популярности шаблона Анемичная модель предметной области

Теперь попытаемся найти ответ на вопрос, почему шаблон Анемичная модель предметной области так популярен. На мой взгляд, основная причина кроется в том, что данный шаблон легче реализуется с помощью современных технологий по сравнению с насыщенной моделью. Наиболее распространенным способом структурирования исходного кода сегодня является шаблон Внедрение зависимостей. При этом в приложении четко прослеживается два источника объектов:
  • IoC -контейнер, который реализует шаблон Инъекция зависимостей и строит служебные объекты: репозитории, сервисы, фасады;
  • ORM -фрейморк, который создает объекты предметной области.
При реализации шаблона Насыщенная модель предметной области возникает проблема связывания объектов, построенных этими двумя способами, в единый граф. Если вернуться к примеру насыщенной модели предметной области, описанному выше, то видно, что в объект класса ServiceOrder нужно каким-то образом передать объект класса, реализующего интерфейс ServiceProviderRepository . Не все ORM -фреймворки позволяют инъектировать в объекты при их построении сторонние зависимости, реализуемые IoC -контейнером. Приходится использовать подходы, обладающие рядом недостатков.
  • Инъекция посредством статических переменных и методов. Репозитории и прочие IoC -объекты передаются как одиночки. Недостатки такого подхода: сложнее подменить объект, например, заменив его на заглушку при тестировании. Так же данный подход вносит скрытые зависимости.
  • Передача зависимостей через метод (не путать с инъекцией через сеттер). Если мы включаем в объект предметной области метод, содержащий бизнес-логику, то в данный метод нужно передавать все требуемые ему объекты: репозитории, фабрики, соединения и т.д. Недостатки такого подхода: усложняется сигнатура методов бизнес-логики, все зависимости выставляются наружу, при усложнении логики метода, ему могут потребоваться новые зависимости, что приведет к изменению сигнатуры метода и необходимости как исправлять вызов метода везде, где он используется, так и как-то передавать новые зависимости в точки вызова.
  • Использовать шаблон Локатор сервисов и передавать каким-то образом реализацию данного шаблона в метод объекта предметной области. Недостатками данного метода являются все недостатки шаблона Локатор сервисов.
  • Вместо зависимостей передавать в метод результат их работы. Но данный подход убирает логику из объекта предметной области и по сути является первым шагом к анемичной модели.

Преимущества шаблона Анемичная модель предметной области

Помимо популярности, вызванной имеющейся инфраструктурой, шаблон Анемичная модель предметной области имеет и ряд собственных преимуществ. Давайте рассмотрим их подробнее.
  1. Простота проектирования и разработки . Как правило Анемичная модель предметной области требует меньше усилий и квалификации для своей разработки. Так как объекты предметной области лишены поведения, которое икапсулируется в сервисах, то снимается вопрос в какой объект предметной области поместить тот или иной метод. Конечно, вместо этого появляются вопросы в какой сервис его поместить, создавать новый сервис или нет, но данные вопросы проще для решения.
  2. Простота генерации на основе хранилища данных : базе данных, WSDL -описанию сервиса, файлу настройки объектно-реляционного отображения и т.д. Данное преимущество особенно ярко проявляется тогда, когда мы строим интерфейс к унаследованной системе, реализованной не на ООП-языке или выставляющей интерфейс в стиле удаленного вызова процедур. При современной нацеленности информационных систем на использование сервисно-ориентированной архитектуры данная особенность подхода играет все большую и большую роль. Это, кстати, отличает текущее положение вещей от времен написания критической статьи Фаулера, в конце концов прошло почти 10 лет. Мне как разработчику клиента к существующей службе предприятия гораздо проще сгенерировать модель данных по ее - службы - контракту (например по WSDL -описанию) и разработать слой классов-менеджеров над данной моделью, нежели строить насыщенную модель предметной области и реализовывать ее интеграцию с удаленной службой.
  3. Простота повторного использования . Если мы имеем приложение, построенное на основе шаблона Анемичная модель предметной области и нам нужно реализовать приложение, работающее с этими же данными, но реализующее другую бизнес-логику, то мы можем переиспользовать классы существующей модели. В случае насыщенной модели предметной области такое переиспользование будет затруднено, т.к. бизнес-логика жестко зашита в классы, реализующие модель предметной области. С данной точки зрения излишняя инкапсуляция скорее вредна нежели полезна.

Заключение

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