Про 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), что делает возможным полиморфизм.
Я не думаю, что исключение внедрения зависимостей (Dependency Injection) на уровне объекта будет сильно способствовать пятому принципу “D” в SOLID (поскольку DI является механизмом реализации DIP), а лишение объекта полиморфизма (особенно в условиях отсутствия Множественной Диспетчеризации и Pattern matching) будет способствовать реализации третьего принципа “L” в SOLID.
В таком случае внедрять зависимости и обеспечивать полиморфизм придется вручную, фактически превращая программу из объектно-ориентированной в процедурную.
📝 The fact that the boundaries are not visible during the deployment of a monolith does not mean that they are not present and meaningful. Even when statically linked into a single executable, the ability to independently develop and marshal the various components for final assembly is immensely valuable.
Such architectures almost always depend on some kind of dynamic polymorphism to manage their internal dependencies. This is one of the reasons that object-oriented development has become such an important paradigm in recent decades. Without OO, or an equivalent form of polymorphism, architects must fall back on the dangerous practice of using pointers to functions to achieve the appropriate decoupling. Most architects find prolific use of pointers to functions to be too risky, so they are forced to abandon any kind of component partitioning.
—“Clean Architecture: A Craftsman’s Guide to Software Structure and Design” by Robert C. Martin
Инкапсуляцию можно делать и в процедурном языке. А вот когда в OOP-языке отдельные индивидуумы отказываются от инкапсуляции, то это вызывает вопросы по поводу их знания основ управления сложностью.
Как говорил Н.Вирт, не сильно одобрявший ООП, - от него польза есть уже только потому, что он побуждает людей использовать инкапсуляцию и сокрытие информации.
💬️ “Многие люди относятся к стилям и языкам программирования как к религиозным конфессиям: если вы принадлежите к одной из них, то не можете принадлежать к другой. Но это ложная аналогия, и она сознательно поддерживается по причинам коммерческого порядка. Объектно-ориентированное программирование вышло из принципов и понятий традиционного процедурного программирования. Скажу больше: в ООП не добавлено ни одного действительно нового понятия; просто по сравнению с процедурным оно делает значительно более сильный акцент на двух понятиях. Первое - это привязка процедуры к составной переменной, что и послужило оправданием для введения терминов “объект” и “метод”. Средством для такой привязки является процедурная переменная (или поле записи - record field), доступная в языках программирования с середины 70-х гг. Второе понятие - это конструирование нового типа данных (названного “подкласс”) путем расширения заданного типа (“суперкласс”).
Стоит заметить, что вместе с ООП пришла совершенно новая терминология, имевшая целью затемнить происхождение его корней. Таким образом, если раньше вы могли инициировать активность процедуры путем ее вызова, то теперь должны посылать сообщение методу. Новый тип строится не расширением заданного, а определением подкласса, который наследует от суперкласса. Это вообще интересный феномен, когда многие люди узнают о таких важных (и древних!) понятиях, как тип данных, инкапсуляция и (возможно) скрытие информации, лишь когда начинают изучать объектно-ориентированное программирование. Что ж, одно это оправдывает излишний шум вокруг ООП, даже если позднее эти неофиты ничего этого и не используют.
Тем не менее, я склонен рассматривать ООП как аспект более общего понятия “программирования в большом” (programming in the large) - тот аспект, что логически следует за “программированием в малом” (programming in the small) и уже поэтому требует надлежащего знания процедурного программирования. Статическая модуляризация - это первый шаг навстречу ООП; этот аспект намного легче понять и освоить, чем полное ООП, к тому же в большинстве случаев этого достаточно для написания хороших программ. Вот почему очень жаль, что этим аспектам в большинстве языков пренебрегли (за исключением Ada).
Я бы не сказал, что распространившаяся практика ООП реализовала все свои потенции. Наша конечная цель - расширяемое программирование (extensible programming). Под этим я понимаю возможность конструирования таких иерархий модулей, когда каждый модуль добавляет новую функциональность в систему. Расширяемое программирование подразумевает, что добавление модуля возможно без необходимости вносить какие-либо изменения в существующие модули - не должно быть необходимости даже их перекомпилировать. Новые модули не только добавляют новые процедуры, но - что более важно - добавляют также новые (расширенные) типы данных. Мы продемонстрировали практичность и экономичность этого подхода при проектировании Oberon System.”
—“Никлаус Вирт о культуре разработки ПО”, Карло Пешио, интервью с Niklaus Wirt
Нужно заметить, что на этом месте многие начинают говорить о превосходствах функционального программирования, зачастую не проводя различий между функциональным программированием и процедурным. Превосходства функционального программирования хорошо осветил Роберт Мартин в статьях “OO vs FP” (2014) и “FP vs. OO” (2018).
Все дело в том, что в функциональном программировании обеспечивается ссылочная прозрачность, т.е. накладывается ограничение на изменяемость данных. А между тем, основной недостаток утраты инкапсуляции в Anaemic Domain Model заключается именно в утрате контроля за изменением состояния и обеспечением инвариантов.
📝 “OO makes code understandable by encapsulating moving parts. FP makes code understandable by minimizing moving parts.” – Michael Feathers
Обе парадигмы, и функциональная, и объектно-ориентированная, решают вопрос управления существенной сложностью (Essential Complexity) программы, но разными способами.
📝 “Brooks argues that software development is made difficult because of two different classes of problems—the essential and the accidental. In referring to these two terms, Brooks draws on a philosophical tradition going back to Aristotle. In philosophy, the essential properties are the properties that a thing must have in order to be that thing. A car must have an engine, wheels, and doors to be a car. If it doesn’t have any of those essential properties, it isn’t really a car.
Accidental properties are the properties a thing just happens to have, properties that don’t really bear on whether the thing is what it is. A car could have a V8, a turbocharged 4-cylinder, or some other kind of engine and be a car regardless of that detail. A car could have two doors or four; it could have skinny wheels or mag wheels. All those details are accidental properties. You could also think of accidental properties as incidental, discretionary, optional, and happenstance.
<...>
As Dijkstra pointed out, modern software is inherently complex, and no matter how hard you try, you’ll eventually bump into some level of complexity that’s inherent in the real-world problem itself. This suggests a two-prong approach to managing complexity:
- Minimize the amount of essential complexity that anyone’s brain has to deal with at any one time.
- Keep accidental complexity from needlessly proliferating.”
<...>
Abstraction is the ability to engage with a concept while safely ignoring some of its details—handling different details at different levels. Any time you work with an aggregate, you’re working with an abstraction. If you refer to an object as a “house” rather than a combination of glass, wood, and nails, you’re making an abstraction. If you refer to a collection of houses as a “town,” you’re making another abstraction.
<...>
From a complexity point of view, the principal benefit of abstraction is that it allows you to ignore irrelevant details. Most real-world objects are already abstractions of some kind. As just mentioned, a house is an abstraction of windows, doors, siding, wiring, plumbing, insulation, and a particular way of organizing them. A door is in turn an abstraction of a particular arrangement of a rectangular piece of material with hinges and a doorknob. And the doorknob is an abstraction of a particular formation of brass, nickel, iron, or steel.
<...>
Encapsulation picks up where abstraction leaves off. Abstraction says, “You’re allowed to look at an object at a high level of detail.” Encapsulation says, “Furthermore, you aren’t allowed to look at an object at any other level of detail.”
—“Code Complete” 2nd edition by Steve McConnell
📝 “Following Aristotle, I divide them [difficulties] into essence - the difficulties inherent in the nature of the software - and accidents - those difficulties that today attend its production but that are not inherent.
<...>
The complexity of software is in essential property, not an accidental one. Hence descriptions of a software entity that abstract away its complexity often abstract away its essence. Mathematics and the physical sciences made great strides for three centuries by constructing simplified models of complex phenomena, deriving properties from the models, and verifying those properties experimentally. This worked because the complexities ignored in the models were not the essential properties of the phenomena. It does not work when the complexities are the essence.”
—“No Silver Bullet - Essence and Accident in Software Engineering” by Frederick P. Brooks, Jr.
Нужно отличать Anemic Domain Model в объектно-ориентированной парадигме от Data Type в функциональной парадигме. Вот здесь, например, сам Eric Evans говорит о том, что в своей книге “Domain-Driven Design” он не рассматривал глубоко функциональную парадигму, потому что в 2003 она не имела такого применения как сегодня. А сегодня, в контексте Event Sourcing, она имеет уже совсем другое значение.
📝 You know, functional is a big thing. Maybe more than one thing. And so there are people though who have been talking about modeling in the functional realm and very interesting things. The things is models are just systems of abstraction. And so you have a powerful mechanism for abstraction. You should be able to implement, you should be able to express models. Furthermore, if you want to, you know, bring that ubiquitous language to life in the code, well, some of the functional languages, I think, are really nice for making making language in the code. And it might be a good mate with Event Sourcing. I’m just sort of laying out like I’m pointing out that we have so many options. Options that were really not there in 2003.
—Eric Evans, “Tackling Complexity in the Heart of Software”, Domain-Driven Design Europe 2016 - Brussels, January 26-29, 2016
Здесь он возвращается к этому вопросу. А здесь Greg Young рассматривает переход от OOP к Functional Programming в Event Sourcing.
Под Anemic Domain Model же понимается вырождение поведения модели именно в объектно-ориентированной парадигме, т.е. использование объектно-ориентированных языков в процедурном стиле.
Также следует отличать Anemic Domain Model от ViewModel, ибо ViewModel по определению не предназначено для какого-либо поведения (а именно неверное расположение поведения является сутью антипаттерна Anemic Domain Model), и часто применяется в CQRS.
Но вернемся к обсуждаемой статье. Я так и не смог обнаружить имя автора той статьи на том сайте. Не уверен, что это как-то могло бы поднять авторитет статьи, которая с такой легкостью берется опровергать статью “Anemic Domain Model” by Martin Fowler. Зато я нередко наблюдал подобный приём с целью привлечения внимания к ресурсу, используя общественную резонансность скандальных утверждений. Может быть в этом все дело? Я не знаю.
Я не наблюдаю в статье четкого понимания автором различий между:
- Логикой уровня приложения
- Бизнес-логикой (причем, следует отличать предметно-ориентированную бизнес-логику от бизнес-логики, зависящей от приложения)
- Обязанностью доступа к данным (что не является бизнес-логикой), иногда именуемой слоем данных
В примере статьи рассматривается вместо бизнес-логики - обязанность доступа к данным (да еще и в виде Active Record). К сожалению, в списке литературы статьи нет другой книги Robert C. Martin - “Clean Code”, в которой рассказывается, как для разделения служебной логики и бизнес-логики вот уже более 10 лет используется Cross-Cutting Concerns.
Выглядит так, что единственный мотив не наделять доменную модель вообще никакими обязанностями - это способность автора всунуть в доменную модель обязанности слоя доступа к данным. К тому же Service Layer относится к Application Logic, т.е. имеет политику более низкого уровня, нежели Domain Logic. А у Domain Service есть ограниченный список причин для своего существования.
В статье приводится неверная трактовка Single Responsibility Principle (SRP), которая подразумевает “делать одну вещь”.
В своей книге Clean Architecture, Robert C. Martin именно по этой причине сожалеет, что выбрал такое название (SRP):
📝 “Of all the SOLID principles, the Single Responsibility Principle (SRP) might be the least well understood. That’s likely because it has a particularly inappropriate name. It is too easy for programmers to hear the name and then assume that it means that every module should do just one thing.
Make no mistake, there is a principle like that. A function should do one, and only one, thing. We use that principle when we are refactoring large functions into smaller functions; we use it at the lowest levels. But it is not one of the SOLID principles—it is not the SRP.
Historically, the SRP has been described this way:
A module should have one, and only one, reason to change.Software systems are changed to satisfy users and stakeholders; those users and stakeholders are the “reason to change” that the principle is talking about. Indeed, we can rephrase the principle to say this:
A module should be responsible to one, and only one, user or stakeholder.
Unfortunately, the words “user” and “stakeholder” aren’t really the right words to use here. There will likely be more than one user or stakeholder who wants the system changed in the same way. Instead, we’re really referring to a group—one or more people who require that change. We’ll refer to that group as an actor.
Thus the final version of the SRP is:
A module should be responsible to one, and only one, actor.Now, what do we mean by the word “module”? The simplest definition is just a source file. Most of the time that definition works fine. Some languages and development environments, though, don’t use source files to contain their code. In those cases a module is just a cohesive set of functions and data structures.
That word “cohesive” implies the SRP. Cohesion is the force that binds together the code responsible to a single actor.
Perhaps the best way to understand this principle is by looking at the symptoms of violating it...”
—“Clean Architecture: A Craftsman’s Guide to Software Structure and Design” by Robert C. Martin
В книге “Agile Software Development. Principles, Patterns, and Practices.” by Robert C. Martin, James W. Newkirk, Robert S. Koss, в оригинальной статье “Principles Of OOD” by Robert C. Martin, и в комментирующей статье “The Single Responsibility Principle” by Robert C. Martin, SRP выводится из понятий Coupling and Cohesion of Constantine’s Law. В то время, как в обсуждаемой статье Cohesion совершенно не учитывается.
Вся эта неразбериха завуалирована введением избыточного понятия Rich Domain Model, что вводит читателя в заблуждение относительно присутствия некой дифференцированности в реализации Domain Model.
Никаких Rich Domain Model не бывает.
💬️ “Модель - это упрощение; это такая интерпретация реальности, при которой из явления извлекаются существенные для решения задачи аспекты, а лишние детали игнорируются.
A model is a simplification. It is an interpretation of reality that abstracts the aspects relevant to solving the problem at hand and ignores extraneous detail.”
—“Domain-Driven Design: Tackling Complexity in the Heart of Software” by Eric Evans, перевод В.Л. Бродового
Само по себе слово “модель” означает упрощение поведения объекта моделирования в рассматриваемом контексте, что противоречит термину “Rich”. Rich Domain Model буквально означает полное воспроизводство объекта моделирования, т.е. его копия.
Популярную фотографию с изображением картонки от холодильника помните? Для того, чтобы понять, пройдет ли холодильник в дверь, нам не требуется полная копия холодильника. Нам требуется его минимально-достаточное поведение в контексте решаемой проблемы.
💬️ “Модель предметной области - Объектная модель домена, охватывающая поведение (функции) и свойства (данные).
Domain Model - An object model of the domain that incorporates both behavior and data.”
—“Patterns of Enterprise Application Architecture” by Martin Fowler, David Rice, Matthew Foemmel, Edward Hieatt, Robert Mee, Randy Stafford, перевод: Издательский дом “Вильяме”
Есть просто Domain Model в виде объекта, инкапсулирующего данные и поведение. А есть структура данных без поведения - это и называется Anemic Domain Model.
В целом, основной мотив сторонников Anemic Domain Model сводится к тому, что, они встречают сложность в разделении реализации служебной Логики Доступа к Данным и Бизнес-Логики Доменной Модели. Поэтому, они предлагают вынести всю Бизнес-Логику из Доменной Модели к служебной логике в Сервисы. Ну... хорошо... допустим... а в Сервисах разве не нужно разделять логику разного уровня политики? Получаются те же яйца, только в процедурном стиле. От перестановки мест слагаемых проблема не решается.
Единственное упрощение, которое можно достигнуть ценой утраты инкапсуляции доменной модели, - это отсутствие потребности в инверсии зависимостей, поскольку сервис уровня приложения, как сервис более низкого уровня политики, может быть осведомлен об интерфейса доменного сервиса, обладающего более высоким уровнем политики. В то время, как доменная модель (в случае применения Lazy Loading) - не может быть осведомлена об интерфейсах более низкого уровня политики, что вынуждает инвертировать осведомленность. Но инкапсуляция позволяет управлять Essential Complexity, что имеет гораздо более важное значение, чем Accidental Complexity. Подробно этот вопрос освещается в статье “Domain model purity vs. domain model completeness” by Vladimir Khorikov.
Главный императив разработки ПО заключается в управлении сложностью. Написание несопровождаемого Spaghetti-code не требует существенных умственных усилий.
📝 “хочу сказать, что сделать простое иногда во много раз сложнее, чем сложное.”
—М.Т. Калашников в интервью журналисту газеты «Metro Москва», 2009 год.
📝 “Усложнять - просто, упрощать - сложно”.
—“Закон Мейера”
Трудности нужно решать, а не замыкаться от них (см. Психологическая Защита).
Мне это напоминает случай, когда Мартину Фаулеру сказали, что гибкое проектирование невозможно, потому что схему базы данных сложно изменить, а значит, ее нужно проектировать заблаговременно. Мартин Фаулер ответил, что если схему базы сложно изменить, значит мы должны подумать о том, как можно сделать процесс миграций проще. Так появился механизм миграций базы данных, который заметно облегчил распространение Agile-модели разработки.
Все что не относится к логике предметной области, - это новая обязанность, которая должна быть вынесена за пределы Domain Model, или, по крайней мере, не рассматриваться как бизнес-логика, если Domain Model реализована в виде паттерна Active Record (как в той статье).
Очень часто можно наблюдать разбухшие модели, которые выполняют очень много несвойственных ее предметной области обязанностей, в т.ч. и уровня приложения (управление транзакциями, проверка привилегий и т.п.). Domain Model должна моделировать только поведение объекта предметной области (реального мира). Если Domain Model имеет несколько десятков методов, которые не выражают поведение объекта реального мира, не имеют общего применения, а используются только одним клиентом, то мы должны их разместить либо непосредственно внутри клиента, либо в классе, который обслуживает клиента (для обслуживания клиентов уровня приложения существует Sevice Layer, для обслуживания клиентов уровня предметной области и выравнивания интерфейсов существует паттерн Wrapper). Более подробно эта тема уже рассматривалась в статье “Проектирование Сервисного Слоя и Логики Приложения”.
Еще одной частой причиной порождения Anemic Domain Model является недостаточное использование Domain Event, либо некорректная его реализация.
Domain Model может быть представлена в виде агрегата, т.е. композиции связанных объектов, что характерно для DDD и NoSQL. Domain Model может иметь методы, изменяющие ее состояние, но она не должна заботиться о его сохранении в базу данных. Предположим, что по мере роста информированности в процессе разработки проекта, возникла потребность перехода с RDBMS на NoSQL-хранилище, что повлекло за собой потребность заменить реализацию класса ответственного за сохранение объекта. С точки зрения архитектуры, база данных - это IO-устройство, от которого приложение стремится быть независимым. NoSQL хранилища построены вокруг идеи агрегата, что позволяет, в определенной мере, избавиться от реляционных связей и упростить масштабирование. Границами транзакции NoSQL-хранилища являются границы агрегата. Если детали реализации сохранения агрегата скрыты за интерфейсом ответственного за это объекта (обычно это Repository + DataMapper), то такой рефакторинг минимизирует изменение самой Доменной Модели. В противном случае, программа не имеет независимости от IO-устройства, что нарушает Single Responsibility Principle (проявляющегося в виде Code Smell “Shotgun Surgery”).
Иногда случается, что Бизнес-Логика Доменной Модели нуждается в доступе к экземпляру связанной Доменной Модели, или даже в доступе к корню другого Агрегата. Недостаточное понимание способов разделения политики разных уровней (Бизнес-Логики и Логики Доступа к Данным) часто приводит к оправданию Anemic Domain Model. Между тем, существует целый ряд способов решения этой проблемы.
Эта тема уже затрагивалась в статьях:
- “Реализация паттерна Repository в браузерном JavaScript“
- “Проектирование Сервисного Слоя и Логики Приложения“
- “Почему я выбираю Storm ORM для Python“
Существует превосходная статья по этому вопросу:
- “Domain model purity and lazy loading” by Vladimir Khorikov
Ключевой признак плохой архитектуры - это ее зависимость от деталей реализации. Архитектура должна определять реализацию, а не подстраиваться под нее.
Да, бывают случаи, когда целесообразней использовать структуры данных вместо объектов. Хорошо эту тему раскрывает Robert C. Martin в главе “Chapter 6: Objects and Data Structures :: Data/Object Anti-Symmetry” книги “Clean Code: A Handbook of Agile Software Craftsmanship”. В Википедии это называется Expression problem. Мне попадалась ещё статья на эту тему: “Что такое expression problem, или о дуализме функционального и объектно-ориентированного программирования” / Дмитрий Дементий. Но эта тема не имеет никакого отношения к предмету обсуждаемой статьи, которая посвящена тому, как писать процедурные программы в Объектно-Ориентированных языках.
Для меня остается загадкой, как можно реализовать в стиле Anemic Domain Model паттерн Class Table Inheritance для коллекции полиморфных объектов с достаточно богатой бизнес-логикой. То же самое справедливо и к паттернам Special Case (aka Introduce Null Object), Replace Conditional with Polymorphism, Replace Type Code With Polymorphism и Replace Type Code with State/Strategy.
Материалы по теме:
- “What is domain logic?” by Vladimir Khorikov
- “Domain services vs Application services” by Vladimir Khorikov
- “Domain model isolation” by Vladimir Khorikov
- “Email uniqueness as an aggregate invariant” by Vladimir Khorikov
- “How to know if your Domain model is properly isolated?” by Vladimir Khorikov
- “Domain model purity vs. domain model completeness” by Vladimir Khorikov
- “Domain model purity and lazy loading” by Vladimir Khorikov
- “In Defense of Lazy Loading” by Vladimir Khorikov
- “Domain model purity and the current time” by Vladimir Khorikov
- “Immutable architecture” by Vladimir Khorikov
- “Link to an aggregate: reference or Id?” by Vladimir Khorikov
- “How to create fully encapsulated Domain Models” by Udi Dahan
Примеры преобразования Anemic Domain Model в Domain Model:
- Refactoring from Anemic Domain Model Towards a Rich One by Vladimir Khorikov
- Refactoring from anemic to rich Domain Model example by Kamil Grzybek
Видео:
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 Jul 29, 2022