Про Anemic Domain Model

Время от времени в кругу моих знакомых регулярно поднимается вопрос о том, что Anemic Domain Model - никакой вовсе и не антипаттерн, и в качестве аргументов приводятся ссылки на статью “The Anaemic Domain Model is no Anti-Pattern, it’s a SOLID design” [1]. После очередного упоминания этой статьи я решил об этом написать.

Я не знаю, читал ли автор той статьи весь список перечисленной внизу нее литературы, потому что в этом списке присутствует книга “Martin, Robert C. Agile software development: principles, patterns, and practices. Prentice Hall PTR, 2003”.

А эта книга дает, на мой взгляд, лучшее представление о том, что делают методы объекта: они Внедряют Зависимости (Dependency Injection), что делает полиморфизм возможным! Я всем советую сперва прочесть эту книгу (именно книгу в оригинале, а не краткие выдержки из Википедии), прежде чем читать указанную статью. Как минимум, просмотрите хотя бы этот видеоролик: “Bob Martin SOLID Principles of Object Oriented and Agile Design”.

Я не думаю, что исключение внедрения зависимостей (Dependency Injection) на уровне объекта будет соответствовать пятому принципу “D” в SOLID, а лишение объекта полиморфизма (да еще и в условиях отсутствия Множественной Диспетчеризации) будет соответствовать третьему принципу “L” в SOLID. В таком случае внедрять зависимости и обеспечивать полиморфизм придется вручную, фактически превращая программу из объектно-ориентированной в процедурную.

Нужно заметить, что на этом месте многие начинают говорить о превосходствах функционального программирования, не понимая отличий межу функциональным программированием и процедурным. Если говорить о превосходствах функционального программирования, то Роберт Мартин уже все сказал в статье “OO vs FP”.

Но вернемся к обсуждаемой статье. Я так и не смог найти имя автора той статьи на том сайте. Нечего и говорить, что это - весомый аргумент для авторитета статьи, которая с легкостью берется опровергать статью Мартина Фаулера(!) “Anemic Domain Model”. Создается впечатление, что подобные статьи - просто способ привлечь внимание к ресурсу, используя общественную резонансность скандальных утверждений.

Автор демонстрирует отсутствие четкого понимания различий между:

  1. Логикой уровня приложения
  2. Бизнес-логикой (причем, следует отличать предметно-ориентированную бизнес-логику от бизнес-логики, зависящей от приложения)
  3. Обязанностью доступа к данным (что не является бизнес-логикой), иногда именуемой слоем данных

Указанная статья целиком построена на ошибках проектирования. В своем примере он рассматривает вместо бизнес-логики - обязанность доступа к данным (да еще и в виде Active Record(!)). Жаль, что в его списке литературы нет книги “Clean Code” by Robert C. Martin, в которой рассказывается, как для разделения служебной логики и бизнес-логики вот уже более 10 лет используется Cross-Cutting Concerns.

Уникальная способность автора всунуть в доменную модель обязанности слоя Data Mapper и Repository, - это, действительно, достаточно весомый аргумент для того, чтобы автор не наделял доменную модель вообще никакими обязанностями. К тому же Service Layer не предназначен для обязанностей предметно-ориентированной бизнес-логики. Мы можем вынести ее куда угодно, в Visitor, Strategy, Wrapper, Delegate, но мы должны соблюдать принципы расслоения системы, и не использовать слой уровня приложения для обязанностей слоя предметной области.

Для того чтобы завуалировать неразбериху, автор вводит лишнее понятие Rich Domain Model, вводя тем самым читателя в заблуждение относительно присутствия некой дифференцированности в реализации Domain Model. Нет никаких Rich Domain Model. Есть Domain Model (объект моделирующий поведение объекта реального мира (предметной области)), а есть Anemic Domain Model (т.е. структура данных, выраженная объектами без поведения).

Все что не относится к логике предметной области, - это новая обязанность, которая должна быть вынесена за пределы Domain Model, или, по крайней мере, не рассматриваться как бизнес-логика, если Domain Model реализована в виде паттерна Active Record (как в той статье).

Очень часто можно наблюдать разбухшие модели, которые выполняют очень много несвойственных ее предметной области обязанностей, в т.ч. и уровня приложения (управление транзакциями, проверка привилегий и т.п.). Domain Model должна моделировать только поведение объекта предметной области (реального мира). Если Domain Model имеет несколько десятков методов, которые не выражают поведение объекта реального мира, не имеют общего применения, а используются только одним клиентом, то мы должны их разместить либо непосредственно внутри клиента, либо в классе, который обслуживает клиента (для обслуживания клиентов уровня приложения существует Sevice Layer, для обслуживания клиентов уровня предметной области и выравнивания интерфейсов существует паттерн Wrapper). Более подробно эта тема уже рассматривалась в статье “Проектирование Сервисного Слоя”.

Domain Model может быть представлена в виде агрегата, т.е. композиции связанных объектов, что характерно для DDD и NoSQL. Domain Model может иметь методы, изменяющие ее состояние или композицию, но она не должна заботиться о сохранении этой композиции в базе данных. Предположим, Вы осознали что NoSQL-хранилище Вам подходит лучше, чем RDBMS, и решили заменить реализацию класса ответственного за сохранение объекта. С точки зрения архитектуры, база данных - это IO-устройство, от которого приложение стремится быть независимым. NoSQL хранилища построены вокруг идеи агрегата, что позволяет, в определенной мере, избавиться от реляционных связей и упростить шардинг. Границами транзакции NoSQL-хранилища являются границы агрегата. Если у Вас детали реализации сохранения агрегата скрыты за интерфейсом ответственного за это объекта (обычно это Repository + DataMapper), то такой рефакторинг не затронет реализацию самой Доменной Модели. Если Вы вынуждены изменять реализацию Domain Model, то это значит, что Ваша программа не имеет независимости от IO-устройства, что нарушает Single Responsibility Principle (в виде Code Smell “Shotgun Surgery”).

Иногда случается, что Domain Model не представлена агрегатом, но должна знать о связанных объектах для реализации своей бизнес-логики. В Python для этого обычно используются дескрипторы, которые осуществляют ленивый доступ к связанным объектам. И хотя это обычно никак не усложняет тестирование (потому что дескрипторы способны кэшировать в памяти явно установленные клиентом связанные объекты), но все-таки привносит в Domain Model служебную логику доступа к данным, пусть она и хорошо инкапсулирована за интерфейсом дескриптора. Более чистым решением является наложение ответственного за связи аспекта на экземпляр Domain Model, сразу после его создания (обычно посредством Dependency Injection). Тогда при тестировании Вы просто подменяете реализацию этого аспекта.

Классический же вариант реализации доступа к связанным объектам, в условиях отсутствия AOP и Дескрипторов, можно увидеть в подразделе “Example: Separating the Finders (Java)” главы “Data Mapper” книги “Patterns of Enterprise Application Architecture” [2]. Но даже в этом случае Доменная модель не превращается в Active Record.

Частично эту тему я уже затрагивал в статьях:

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

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

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

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

Автор просто пишет о том, как писать процедурные программы в Объектно-Ориентированных языках.

Попробуйте реализовать в таком стиле паттерн Class Table Inheritance для коллекции полиморфных объектов с достаточно богатой бизнес-логикой, и вы поймете все недостатки Anemic Domain Model. То же самое справедливо и к случаю использования паттерна Special Case, известного так же как метод рефакторинга Introduce Null Object. Смотрите так же Replace Conditional with Polymorphism, Replace Type Code With Polymorphism и Replace Type Code with State/Strategy (желательно смотреть информацию в книге, номер страницы указан на страницах онлайн-каталога по ссылкам).

Footnotes

[1]“The Anaemic Domain Model is no Anti-Pattern, it’s a SOLID design” https://blog.inf.ed.ac.uk/sapm/2014/02/04/the-anaemic-domain-model-is-no-anti-pattern-its-a-solid-design/ (перевод на русский “Анемичная модель предметной области — не анти-шаблон, а архитектура по принципам SOLID” https://habrahabr.ru/post/346016/ )
[2]“Patterns of Enterprise Application Architecture” by Martin Fowler, David Rice, Matthew Foemmel, Edward Hieatt, Robert Mee, Randy Stafford

Updated on May 16, 2018

Comments

comments powered by Disqus