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


Оригинал: The Anaemic Domain Model is no Anti-Pattern, it’s a SOLID design


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


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


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


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


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


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


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


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


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


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


Давайте представим серверную часть интернет-магазина, где клиент может как покупать товары, так и выставлять на продажу товары для других клиентов со всего земного шара. Приобретение товара приводит к уменьшению средств на счету покупателя. Подумаем, как можно реализовать процесс размещения клиентом заказа на приобретение товара. Согласно требованиям, клиент может разместить заказ, если у него а) достаточно средств на счету, и б) товар доступен в регионе клиента. При использовании БМПО, класс 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 [17], в котором используются методы 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 и их наложения на обе рассматриваемые архитектуры [12]. «S» означает «Принцип единственной ответственности» (Single Responsibility Pronciple, [13]), который гласит, что класс должен делать только что-то одно — но делать это хорошо. В частности, класс должен реализовывать лишь одну абстракцию. «O» — «Принцип открытости/закрытости» (Open/Closed Principle, [14]), постулат о том, что класс должен быть «открытым для расширения, но закрытым для изменения». Это означает, что при разработке класса надо максимально стремиться к тому, чтобы реализацию не пришлось изменять в будущем, тем самым сводя к минимуму последствия вносимых изменений.


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


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


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


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


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

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


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


В рассмотренном примере проблема закрепления за одним классом не связанных между собой ответственностей, с которой мы столкнулись в БМПО, эффективно решается в анемичной модели при помощи букв I и D из аббревиатуры SOLID. Это, я напомню, «Принцип разделения интерфейса» (Interface Segregation Principle, [15]) и «Принцип инверсии зависимостей» (Dependency Inversion Principle, [16]). Они утверждают, что интерфейсы должны представлять собой наборы сильно сцепленных методов, и что интерфейсы должны использоваться для соединения частей системы воедино (в случае АМПО — соединение служб доменного слоя между собой). Следование принципу разделения интерфейса, как правило, дает в результате небольшие, узкоспециализированные интерфейсы — такие как 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, [17], [18]), посредством которых происходит обмен со слоем хранения данных. Они разнесут в пух и прах идею прямого вызова конструктора класса Order непосредственно из логики класса Customer — разумеется, ни в одной вменяемой реализации сущности предметной области не будут вызывать конструктор напрямую, здравый смысл заставляет использовать фабрику [5]! Но по мне, это выглядит как попытка применять мощь принципов SOLID к инфраструктурным службам, при полном их игнорировании в приложении к модели предметной области. Если нашу гипотетическую БМПО рефакторить для соответствия принципам SOLID, будут выделены более мелкие сущности: из сущности Клиент могут быть выделены сущности «Покупка клиента» (CustomerPurchase) и «Возврат ден.средств клиента» (CustomerRefund). Но может статься, что и новые модели будут по-прежнему зависеть от элементарных бизнес-правил, изменяемых независимо друг от друга, а от них, в свою очередь, будут зависеть другие сущности. Во избежание дублирования логики и сильной связанности классов эти правила придется и дальше рефакторить, выделяя их в отдельные модули, доступ к которым осуществляется посредством интерфейсов. В итоге, богатая модель, отрефакторенная до полного соответствия принципам SOLID, стремится к состоянию анемичной модели!


Заключение


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


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


Ссылки


Развернуть

[1] Fowler, Martin. Anaemic Domain Model. http://www.martinfowler.com/bliki/AnemicDomainModel.html, 2003.


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


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


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


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


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


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


[8] Budgen, David. Software design. Pearson Education, 2003.


[9] Scott, Michael L. Programming language pragmatics. Morgan Kaufmann, 2000.


[10] Hevery, Misko. Writing Testable Code. http://googletesting.blogspot.co.uk/2008/08/by-miko-hevery-so-you-decided-to.html, Google Testing Blog, 2008.


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


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


[13] Martin, Robert C. SRP: The Single Responsibility Principle. http://www.objectmentor.com/resources/articles/srp.pdf, Object Mentor, 1996.


[14] Martin, Robert C. The Open-Closed Principle. http://www.objectmentor.com/resources/articles/ocp.pdf, Object Mentor, 1996.


[15] Martin, Robert C. The Interface Segregation Principle. http://www.objectmentor.com/resources/articles/isp.pdf, Object Mentor, 1996.


[16] Martin, Robert C. The Dependency Inversion Principle, http://www.objectmentor.com/resources/articles/dip.pdf, Object Mentor, 1996.


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


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


[19] Fowler, Martin. Repository. http://martinfowler.com/eaaCatalog/repository.html, Martin Fowler site, 2002.


[20] Fowler, Martin. Domain Model. http://martinfowler.com/eaaCatalog/domainModel.html, Martin Fowler site, 2002.

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


  1. flancer
    04.01.2018 00:56

    Более того, мне даже кажется, что за анемичной моделью будущее. Все приложения, по большому счету, создаются для обработки данных. И представление разработчика о предметной области (модель) — это всего лишь одно из множества возможных представлений (моделей). Эту мысль замечательно со всех сторон показывает коллега maxstroy в цикле своих статей.
    Мы никоим образом не сможем избежать хранения данных, но мы можем избежать связывания данных с правилами их обработки. Ничего не имею против ООП, но я также ничего не имею и против ФП. Просто, чем сложнее проектируемая предметная область, тем выше шанс, что придется один и тот же набор данных рассматривать с точки зрения разных моделей. И чем раньше отделить данные (то, что хранится в базе, грубо говоря) от обработчиков (то, что существует в программном коде), там меньше вероятность применить имеющуюся модель (объект = данные + поведение) в непредназначенных для этой модели (поведения) условиях.


    1. VolCh
      04.01.2018 03:50

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


  1. crea7or
    04.01.2018 02:59

    Не надо переводить pattern как шаблон, С++ программисты не одобрят. Эти термины давно переводятся(читаются) как есть — паттерн и антипаттерн.


    1. pankraty Автор
      04.01.2018 14:56

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


    1. prowwid
      04.01.2018 18:50
      +1

      Посмею заявить, что не С++ программисты не имеют ничего против слов «шаблон» и «анти-шаблон».

      Абсолютно соглашусь, что паттерн и антипаттерн стали обычными разговорными словами у разработчиков но, на сколько мне известно, во многих вузах преподаватели говорят «шаблон», а не «паттерн».
      По крайней мере когда я учился то преподаватели еще импользовали слово «шаблон».
      Собственно начинающие разработчики скорее всего будут знать «шаблоны», а не «паттерны».

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

      P.S. не подумайте что я с Вами спорю — просто хотелось изложить свою точку зрения и наблюдения.


      1. crea7or
        04.01.2018 19:03

        Но известнейшая книга Гамма (приведена в статье) «Приемы объектно-ориентированного проектирования. Паттерны проектирования». Да и в самой статье то паттерны, то шаблоны употребляется.


        1. prowwid
          04.01.2018 19:40
          +1

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


        1. visirok
          04.01.2018 23:11

          В одном интервью он сказал, что сейчас очень многое написал бы по-другому. Особенно главку про Singleton.


          1. crea7or
            04.01.2018 23:16

            В плюсах, например, согласно C++ Core Guidelines очень много теперь по другому. И синглтонов надо избегать. Всё меняется-то.


  1. VolCh
    04.01.2018 04:08

    На первый взгляд выглядит, что богатая модель искусственно перегружена ответственностями — оба метода слабо относятся к сущности Customer, по-моему, особенно метод покупки. А ActiveRecord многими сам по себе считается антипаттерном.

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


    1. emacsway
      04.01.2018 20:32

      VolCh, Совершенно верно!pankraty, спасибо за перевод. В кругу моих друзей уже не первый раз всплывает эта тема, поэтому решил ответить по поводу Anemic Domain Model здесь.


  1. aamonster
    04.01.2018 04:25

    /me не понял, почему нельзя классы, обеспечивающие инварианты, сделать как обёртку к "анемичным"? Вроде и волки сыты, и овцы целы, и пастуху вечная память — модель, "богатая" снаружи и "анемичная" внутри.


    1. Nashev
      06.01.2018 00:56

      Дык вроде автор по факту и показывает, что если богатую модель вести в SOLID, ровно то что вы пишете и выйдет)


  1. OlegYch_real
    04.01.2018 06:57

    это по сути функциональный подход, где данные и методы разделены
    мне (и не только) кажется тут более будет понятен в контексте ООП такой принцип — чем меньше у метода параметров/зависимостей (в том числе неявных в виде полей и методов классов) — тем лучше
    плюс в идеале у метода не должно быть ненужных параметров
    пример БМПО в таком случае очевидно хуже так как каждый метод принимающий Item неявно зависит от логики purchase, а в примере АМПО такая зависимость будет явно указана через PurchaseService


  1. garex
    04.01.2018 06:59

    Автор статьи забыл сказать с т.з. чего нечто является антипаттерном.


    С т.з. ООП — это явный антипаттерн, т.к. ООП означает поведение, когда объекты обмениваются сообщениями, вызывая методы и во главу угла ставится поведение, а данные прячутся.


    Анемичность же — явный признак не ООП, а его "соседа" ADT. Читаем для примера http://www.cs.utexas.edu/~wcook/papers/OOPvsADT/CookOOPvsADT90.pdf


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


    Любая красивая, гармоничная и качественно спроектированная система не даётся с ходу. Это либо опыт проектировщика, либо банально эволюция.


    1. jakobz
      04.01.2018 11:44

      Все еще проще. Классическое ООП = «Rich Data Model» = антипаттерн. А «Anemic Data Model» — это способ адептов ООП сказать «не пишите на ООП, берите ФП», не признаваясь что вся концепция «объект сам с собой что-то делает» — провалилась.


      1. prowwid
        05.01.2018 13:01

        Я с Вами не соглашусь.

        Классическое ООП = «Rich Data Model» = антипаттерн.

        Это заявление выглядит как типичное «ваше ООП отстой, а вот ФП — это круто».

        Говнокодище пишут и на ООП языках и на ФП — проблема, в основном, в людях, а не в ООП или ФП.

        К примеру, люди берут типичный MVC framework для Enterprise системы, засовывают всю бизнес логику в AcctiveRecord, а потом мы видим тысячи гневных отзывов о AcctiveRecord.
        Но в то же время тысячи людей используют типичный MVC framework с AcctiveRecord для небольших систем и вполне себе счастливы, а заказчик получает новый фичи по рассписанию.

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


        1. VolCh
          05.01.2018 13:19

          Просто с засовыванием бизнес-логики в ActiveRecord ещё можно мириться в достаточно больших приложениях, он для того и создан. Но вот когда в одном методе переплетается и бизнес-логика, и логика хранения (как в примере в статье), вот тут уже тушите свет…


          1. prowwid
            05.01.2018 14:45

            VolCh Вы естественно правы — я обобщенно говорил.

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

            Для примера и привел ActiveRecord т.к. с ним очень часто допускают ошибки и, как Вы и сказали, начинают делать лапшу из бизнес и логики хранения. В итоге получаются классы на 1500 — 4000 тысячи строк где код на все случаи жизни с переплетением бизнес правил и логики ОРМ.


            1. Mendel
              05.01.2018 15:56
              +1

              99% подобных проблем лечится даже не архитектурой а стайлгайдом.
              Не приниать на код-ревью классы где в методах больше 20 строк а классы больше 200 строк. Ну не строго конечно, но превышения должны быть единичны и иметь вескую причину.
              Когда они начнут их дробить на слои создавая из лапши лазанью наследования, у них хоть чуток начнут шестеренки крутиться на тему структуры. Но даже если лазанья останется, а не появится желание разделять сущности, то все равно с таким макаронным монстром жить будет проще. Разумеется если соблюдены хоть какие-то нормы именования и компановка хоть какую-то логику имеет.


            1. VolCh
              05.01.2018 16:18

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


              1. prowwid
                05.01.2018 16:26

                VolCh я с вами согласен.

                Действителльно припекает зачастую тем, кто получает в подарок ведро лапши.

                Но добавлю, что порой даже те, кто начал варить лапшу, понимают что они наделали, но винят в этом ActiveRecord, framework и т.п. (сам грешен — так делал в прошлом)

                А насчет Вашего описания проблемы с использованием ActiveRecord — я об этом и думал, но не расписал более детально как Вы.


                1. VolCh
                  05.01.2018 16:40

                  Не понимаю, как можно винить паттерн или фреймворк, если ты сам решил его применять, особенно понимая, что применяешь ты его неправильно :)


                  1. pankraty Автор
                    05.01.2018 16:47

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


                  1. prowwid
                    05.01.2018 16:59

                    добавлю к коментарию pankraty

                    представим что есть Junior или Middle разработчик. Ему дают задачу и он ее рализует основываясь на подходах принятых в текущем framework'е. Потом идет следующая задача, за ней следующая и в какой-то момент приложение разрастается, а подходы продолжают применяться как для более простого варианта и со временем все скатывается в помойку.

                    Разработчик делает вывод, что виноват framework.


                    1. VolCh
                      05.01.2018 17:47

                      В том, что его не предупредили вовремя, что пора менять фреймворк или, хотя бы, подходы?


                      1. prowwid
                        05.01.2018 18:17

                        думаю можно и так сказать.

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

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

                        К примеру, фирма конвеером клепает простенькие заказы на Yii2 и вдруг заходит что-то серьезное. Скорее всего начнут делать как для конвеера и наберут кучу проблем т.к. надо было отказаться сразу от Yii2, а скорее всего и от PHP. (это так, пример из пальца чтобы передать суть)


  1. ilitnyexpert
    04.01.2018 09:44
    -2

    архитектура по принципам SOLID

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

    Чтобы грамотно спроектировать систему, нужно иметь достаточный опыт проектирования систем, а так же видеть много примеров качественной архитетуры уже готовых систем. А не заучивать что означают буковки в аббривеатуре SOLID.

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

    Например, с чего вы вообще решили что архитектура которая соотвествует принципам SOLID — это хорошая архитектура?


  1. sovaalexandr
    04.01.2018 09:44
    +2

    Комментирование к оригинальной статье закрыто, но это не означает что перед распространением этой статьи не нужно было ознакомиться с комментариями к ней. В этих комментариях ясно и лаконично определено какие именно ошибки в проектировании (да, именно ошибки), привели автора к этой идее. Автор оригинальной статьи не способен смоделировать предметную область и ошибки в этом моделировании валит на RDM. Автор оригинальной статьи явно не знаком с трудами Роберта Мартина в полной мере. Сам Дядя Боб неоднократно высказывался против применения Simple Data Structures как основного элемента программ. Его примеры и обьяснения принципов изобилируют применением инстансов, которые несут операции, а не данные. Да, он сам говорил что для внутренней реализации могут быть использованны данные, но контракт юнита не должен давать к ним прямого доступа.
    Далее: автор оригинальной статьи использует ActiveRecord, наследует сущьность и говорит что это из-за RDM он не может легко заменить хранилище на то, которое не поддерживается его базовым классом. Тем самым нарушая рекомендации (см. пункт Persistence) Uncle Bob'а и снова сваливая всю ответственность на RDM.
    Тестируемость: Uncle Bob, как большой поклонник и пропагондист TDD, разумеется посвятил этой теме не мало материала, с короым автор оригинальной статьи так же не ознакомился перед её написанием. Тестироваться, прежде всего, должен контракт, а не внутренняя структура (детали реализации) т.к. детали реализации могут меняться, но контракт должен выполняться, о чём как Роберт Мартин так и Мартин Фаулер и Стив Макконелл неоднократно писали. Автор же, нарушая essence тестирования и покрывая функционал тестом "лишь бы тесты были" принципиально не правильно применяет тестирование получая сложность test maintain и, как и везде, обвиняя в этом RDM, а не себя.
    Предложеный в статье рефакторинг, который бы привёл к состоянию близкому к ADM так же не является оптимальным. К таким последствиям может привести использование в приложении структуры данных идентичной к той в которой данные хранятся в постоянном хранилище, что не является обязательным. Структура, в которой данные хранятся (напр. таблица и row в ней), не должна влиять на дизайн самого приложения.


  1. myrkoxx
    04.01.2018 10:35

    Как по мне, то в коментариях к оригинальной статье верно подметили:

    1. В примере БМПО связан уровень доступа к данным и уровень бизнес-логики
    2. БМПО спроектировано плохо:

    «Когда клиент отправляется в магазин, он держит товар/товары и отправляет их в кассу, чтобы купить их.Это не ответственность клиентов, чтобы сказать, может ли он купить свои товары, это ответственность сопутствующего лица в кассе.»

    Оригинал:

    blog.inf.ed.ac.uk/sapm/2014/02/04/the-anaemic-domain-model-is-no-anti-pattern-its-a-solid-design/#comment-205


  1. vasIvas
    04.01.2018 11:02
    -2

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

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

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

    Так же, говоря о моделях, очень важно обозначать архитектуру, так как анимичная модель не может существовать в mvc или mvvm, а другие в mvp. Другими словами, модель обуславливает саму архитектуру и смешивать все в одну кучу не верно.


    1. Naglec
      04.01.2018 13:29
      +1

      Логика в контроллерах? Оооок


  1. PerlPower
    04.01.2018 11:12

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


    1. 5mbytes
      04.01.2018 14:04
      +2

      > не нужно искать хэлпер для форматирования какого-то поля объекта

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


      1. INC_R
        05.01.2018 01:08

        Это зависит, мне кажется. Например, если обращение к пользователю по имени различается в зависимости от каких-то факторов (скажем, до 18 лет — ФИ, 18+ — ФИО) — это может и с натяжкой, но бизнес-логика.


        И мне лично в таких случаях было бы комфортнее писать person.Name.OurSpeciallyFormattedName, а не new PersonNameFormatter().FormatOurSpeciallyFormattedName(person).
        Сущность Person это не особо забивает, потому что из нее торчит не простыня форматов имени на все случаи жизни, а только что-то вроде public PersonName Name => new PersonName(this), и именно этот PersonName инкапсулирует всю логику, связанную с разными форматами имени. Вроде как такой торчащий из сущности PersonName — это пример Value Object, хотя тут я могу заблуждаться.


        На истину не претендую; если такой подход чем-то плох, рад был бы услышать критику.


        1. bm13kk
          05.01.2018 02:55

          Простая интернализация сразу смешивает все карты. Первое, что приходит в голову: разный возраст зрелости, разный порядок имен в зависимости от страны.


          Проблемы бизнес логики (в смысле та логикка, что деньги генерирует, а не слой в MVC) она постоянно меняет список знаний, необходимых для принятия решения.


        1. garex
          05.01.2018 08:26

          Закон Деметры. Статья 1, п. 1.


        1. VolCh
          05.01.2018 12:25

          С большой натяжкой. И, главное, это провоцирует добавление этой самой простыни для всех случаев жизни. Вы сами может и не добавите, а кто-то другой, увидев, что вы добавили OurSpeciallyFormattedName, добавит для другого случая, например для отчётов в налоговую FiscalFormattedName, где ФИО надо выводить полностью. А потом пенсионный попросит не ФИО, а ФИ.О… И это в рамках одной юрисдикции, при неизменяющихся требованиях собственно бизнеса.


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


          1. prowwid
            05.01.2018 14:37

            Для описанных случаев я бы ни VO ни хелпер не использховал.

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

            Давайте разделим простые примеры и более сложные.

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

            public class Person
            {
                private string firstName;
                private string lastName;
               
                public Person(string first, string last)
                {
                    firstName = first;
                    lastName = last;
                }
            
                public string Name => $"{firstName} {lastName}";   
            }
            

            Другое дело, если, к примеру, нам нужно будет показывать имя Person на сайте и в отчете, применяя разные методы форматирования.
            Тогда можно обратиться к адаптерам/моделям отображения или т.п., но никак не к VO и не добавлять логику для отображения и форматирования для каждого места использования в сущность. Получится что-то наподобие:
            public class Person
            {
                public string FirstName 
                { get; set; }
                public string LastName 
                { get; set; }
               
                public Person(string first, string last)
                {
                    FirstName = first;
                    LastName = last;
                }
            }
            
            public class WebPerson
            {
                private Person person;
               
                public Person(Person personEntity)
                {
                    person = personEntity;
                }
            
                public string Name => $"{personEntity.FirstName} {personEntity.LastName}";   
            }
            
            public class ReportPerson
            {
                private Person person;
               
                public Person(Person personEntity)
                {
                    person = personEntity;
                }
            
                public string Name => $"First Name: {personEntity.FirstName} | Last Name: {personEntity.LastName}";     
            }
            


            А если добавится интернализация, что-то наподобие:
            
            public class InternationalPerson
            {
                private Person person;
               
                public Person(Person personEntity)
                {
                    person = personEntity;
                }
            
                public string Name => PersonNameFormatter.format(person);     
            }
            


            Не подумайте, что вышеприведенные примеры являются однозначно правильным путем для реализации. Это просто псевдо-С# код который показывает общую идею.

            Я хочу сказать, что операции над данными сущности не обязательно должны быть в самом классе сущности но это и не значит, что их все надо выносить в сервисные классы.
            Как уже сказал VolCh, сущности не должны отвечать за свое отображение.
            В сущности должен находиться код, который работает с данными сущности в рамках бизнес логики.
            К примеру, подобные методы вполне могут быть в сущности или VO сущности:
            person.isActive();
            person.Status.IsActive();
            person.Activate();
            person.IsAdult()
            


            Надеюсь мой посыл понятен. Если нет, я приведу примеры получше.


            1. VolCh
              05.01.2018 16:36

              Выделить Name в value object свойство объекта Person с одним стандартным преобразование в строку и, возможно, методом типа format(string format) в целом хорошая идея, по-моему, как по смыслу предметной области "физическое лицо", так и по частым юзкейсам в различных системах. Случаев когда нам нужно только имя или только фамилия, не для целей презентации или поиска значительно меньше, чем случаев, когда мы оперируем им как одним целым и сравниваем разные имена по полному совпадению имени. Банальные даты и то чаще нужно по частям анализировать, чем имена, а представление их как единого целого (VO с возможностью получить день, месяц или год или просто строка единого формата) можно сказать стандарт де-факто.


              1. prowwid
                05.01.2018 17:07

                согласен.

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

                Насчет Name как value object с одним стандартным преобразование в строку, да это хороший вариант, для некоторых случаев избыточен но если говорить обобщенно, то я был бы рад иметь Name, Date, Status и т.п. как VO по умолчанию.

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


  1. Urgen
    04.01.2018 13:37

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


    1. VolCh
      04.01.2018 14:30

      Классы сущностей — одно из двух основных мест для размещения бизнес-логики. Второе — сервисы (доменные). Грубое правило: то, что изменяет одну сущность, в неё и помещается, что несколько — в сервис. Больше особо и помещать некуда, остальное, по идее, или инфраструктура, или логика приложения.


      1. Urgen
        04.01.2018 15:31

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


        1. pankraty Автор
          04.01.2018 15:43
          +1

          Похоже, вы тоже используете подход с анемичной моделью, особо не задумываясь об этом. Удивлены, что это, оказывается, «антипаттерн»?


          1. Urgen
            04.01.2018 16:34

            Удивлён. А ещё удивлён, что здесь вообще используются понятия «паттерн» и «антипаттерн». В моём понимании паттерн — готовое решение для какой-то проблемы, которое можно вписать в любую архитектуру, решение более высокого уровня. Так что впору вводить понятие «антиархитектура»)


            1. VolCh
              04.01.2018 19:33

              Паттерн — хорошее готовое решение, антипаттерн — плохое :)


      1. visirok
        04.01.2018 23:26

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


        1. VolCh
          05.01.2018 12:35

          Наружу нужно выносить логику взаимодействия объектов, не укладывающихся в композицию, в has-a. За своё собственное состояние и состояние своих "детей" объект должен отвечать сам. Сложные взаимосвязи изменений можно реализовать разными способами, простыми сценарными скриптами, например, или доменными событиями, обработчики которых преобразуют события в последовательность вызовов разных методов разных объектов модели.


          1. visirok
            05.01.2018 15:07

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


            1. vintage
              05.01.2018 15:28
              +1

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


              1. VolCh
                05.01.2018 16:44

                Хороший пример :) Единственное, ещё они взаимодействуют с ним путём реакции на его публичные события, как бы подписываясь на них, если ребёнок рядом с ними. А так же, когда ребёнок дергает их публичные методы.


              1. visirok
                05.01.2018 17:55

                Речь не о ребёнке. Если надо имплементировать что-то подобное. Где должен находиться метод, выдающий решение, что с ним (ребёнком) в данной ситуации делать?
                Догматически — в классах мамы, папы, соседа, прохожего или в их общем super (в терминологии Java).
                Прагматически — во внешнем сервисе.
                Но это моё личное мнение. Я Вам его не навязываю.


  1. alexs0ff
    04.01.2018 13:43
    +1

    Мое ИМХО основанное на опыте. Анимичная модель отлично справляется там, где архитектура подвержена изменениям. Можно иметь несколько вариантов сервисов, причем переключаться между ними несколько раз на дню. Если же приложение — монолит, где происходит в основном фикс багов, то да удобнее держать логику и данные в одном месте.


    1. flancer
      04.01.2018 14:19

      там, где архитектура подвержена изменениям

      Замечательное замечание! Именно поэтому я и говорю, что за анемичной моделью будущее — по крайней мере, в web-приложениях, где я обитаю. Там изменения всё время как из рога изобилия.


    1. VolCh
      04.01.2018 15:00

      Как по мне, то анемичная модель или богатая — это часть архитектуры, причём важная часть. А если говорить о бизнес-логике, то как бы и не самая важная.


    1. pankraty Автор
      04.01.2018 15:40

      Соглашусь. Я бы еще дополнил таким соображением: если «приложение» — не самодостаточная система, а некая библиотека, решающая прикладную задачу с более менее четко очерченными границами (например, работа с XLS-файлами), то для нее использовать богатую модель с продуманной разветвленной иерархией классов является вполне оправданным. Благодаря тому, что границы намечены заранее, вполне реально запроектировать систему достаточно гибкой, чтобы адаптироваться под новые требования, при этом возникновение таких требований, которые поломают всю структуру, довольно маловероятно.
      На противоположном полюсе я бы поставил энтерпрайз системы, особенно «полукоробочного» плана, когда компании продается готовый продукт, подвергаемый серьезной модификации под нужды конкретного заказчика. Заранее предусмотреть все требования даже одного заказчика — архисложная задача. А уж сделать так, чтобы будущие требования будущих клиентов без проблем укладывались в существующий продукт (который бы при этом не был «голым» фреймворком) — еще сложнее. Рано или поздно обязательно окажется, что допущения, справедливые для 10 клиентов, не соответствуют бизнес-модели 11-го. Для таких продуктов, как мне кажется, анемичная модель подходит весьма хорошо. В коробочном продукте реализуется базовая логика, отвечающая потребностям большинства заказчиков, а там, где логику надо переопределить, сделать это легко — достаточно подменить соответствующую службу, никак не нарушая работы остального кода.

      Для других приложений может быть найдено место на этой оси где-то между двумя полюсами. Они оба имеют право на существование. Но когда один называют «паттерном» (_«Так делать правильно!»_), а второй — «анти-паттерном» (_«Дети, не делайте так!»_), то, ИМХО, второй подход незаслуженно маргинализируется.

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


  1. lair
    04.01.2018 19:23
    +2

    Мне вот каждый раз любопытно: а чем же, концептуально, подход "анемичная модель + сервисы" отличается от, собственно, функционального программирования (ну, за исключением мутабельности)?


    1. velvetcat
      05.01.2018 23:40

      Концептуально — наличием переменных, операции присваивания, императивностью. Короче, всем. А еще в функциональных языках есть другие характерные плюшки: абстрактные типы данных, выведение типов компилятором, паттерн матчинг и пр. Не говоря уж о стремлении в них к четкому разделению на чистые и нечистые функции (и минимизации числа/размера последних).


      (Не вам) Так что не надо анемичные модели оправдывать крутостью ФП. Анемичные модели — это неправильное использование ООП, и не более того.


      1. Naglec
        05.01.2018 23:53

        А у вас в веб-приложения прям таки «бохатая модель» везде? Я хотел бы на это посмотреть.
        У меня лично (может я и горе-проектировщик, конечно) в контексте больших постоянно развивающихся ASP.NET MVC приложений нормально уживаются именно сервисы с логикой + анемичная модель.
        В небольших приложениях (особенно НЕ веб) с ограниченной, заранее ясной бизнес-логикой хорошо взлетает «бохатая модель»/DDD/SOLID/ООП и прочие радости.

        Научите меня как строить крупные ынтырпрайз веб приложения с постоянно меняющейся бизнес-логикой на основе Rich Data (Domain?) Model. Книжки, статьи, примеры. Спасибо.


        1. velvetcat
          06.01.2018 01:06

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


          Для простого бложика сойдет любая парадигма, потому что мало кода можно распределить как угодно. Active Record, только end-to-end тесты.


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


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


          Короче, выделяйте то, что меняется, и держите вместе то, что меняется вместе.


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


          И научитесь как бы находиться в двух режимах — программирование на уровне объектов и программирование самих объектов.


          1. Naglec
            06.01.2018 01:27

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

            Т.е. вот у нас есть Order. Бизнес начинает придумывать разные хитрые правила изменения состояния заказа в зависимости от погоды, дня недели и цвета чулок Марьиванны. Куда мы пихнем эти правила? Куда-то наверх. При этом модель будет анемичной, IOrderProcessingService будет являться фасадом, а внутрях у ней неонка с фабриками, стратегиями, репозиториями марьиванн, хитрыми пайплайнами и прочими классными шаблонами.

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


      1. lair
        05.01.2018 23:56

        Концептуально — наличием переменных, операции присваивания, императивностью.

        Это как раз не "концептуально", потому что на уровне "объекты и сервисы" всего этого нет.


        1. velvetcat
          06.01.2018 00:11

          Как это нет? А с чем тогда работают сервисы, как не с переменными?


          Анемичная модель — это процедуры и переменные-структуры. А вот в ФП вообще нет доступного программисту напрямую состояния.


          1. Naglec
            06.01.2018 00:19

            Я не очень согласен в вашим определением «сервисов + анемичной модели».
            Мне это все видится скорее как:
            Сервисы (фасады) -> некоторая объектная модель с логикой ->
            анемичная модель (сущности) как способ хранить состояние. Возможно, с примитивной логикой в рамках одной сущности

            Это то, как лично у меня получается жить в современном ынтырпрайзе с ORM/DDD и прочими штуками. Да, это не кошерное ООП, но оно работает в вебе. There is no silver bullet, все дела.


            1. velvetcat
              06.01.2018 01:13

              Я не очень согласен в вашим определением

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


              1. Naglec
                06.01.2018 01:16

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


          1. lair
            06.01.2018 01:00

            А с чем тогда работают сервисы, как не с переменными?

            С пришедшими данными.


            Анемичная модель — это процедуры и переменные-структуры.

            Совершенно не обязательно. Анемичная модель — это модель, лишенная логики. Ничто не мешает ей быть immutable.


            1. velvetcat
              06.01.2018 01:08

              Я имею ввиду, что анемичные модели — это переменные. Сервисы — процедуры, которые работают с этими переменными. Это ни разу не ФП.


              1. lair
                06.01.2018 01:31

                Я имею ввиду, что анемичные модели — это переменные.

                Модель — не переменная. Модель — структура данных.


  1. y90a19
    04.01.2018 23:01
    +1

    Мои Best Practicles

    1) Объекты только в виде POJO, никакой логики внутри. Ибо если внутрь класса класть бизнес-логику, он быстро превращается в god object, внутри появляется очень много зависимостей и зависимостей от вызовов методов. Плюс нарушается принцип инкапсуляции — зона ответственности весь класс и все потомки, нет четких границ. В случае же процедурного-подобного подхода зона ответственности четко ограничена, и четко видны вход и выход.
    Плюс объекты с бизнес-логикой внутри сложно тестировать, ибо оно зависит от внутренних состояний

    2) Бизнес-логика в виде процедурно-подобных методов в классах-сервисах. Легко тестировать, легко расширять, легко видна зона ответственности, легко делится на слои, легко разделяется на разные классы

    3) Наследование только в виде расширения POJO объектов, в соответствии с первым пунктом
    4) Делим приложение на слои, минимальное количество: слой записи, слой чистой бизнес-логики и слой взаимодействия с внешним миром

    5) Юнит-тестирование только в случае непонятного или страшного кода, в котором я не уверен. Гнаться за 100% покрытием — вредно, ибо увеличивает кодовую базу и затрудняет рефакторинг и правки.
    Вместо юнит-тестов по максимуму интеграционные тесты, прогоняющие реальные сценарии работы с начала и до конца


    1. visirok
      04.01.2018 23:42

      С пунктами 1-4 почти полностью согласен, а вот с пунктом 5 нет. По следующим причинам:
      1. В прогонных тестах трудно создать условия, которые тестируют важные аспекты поведения. Например, что система правильно себя поведёт в определённой ситуации.
      2. Если прогонно-интеграционный тест перестает работать, бывает трудно понять почему именно. Ошибка может показать себя позже появления или вообще в конце.
      3. Когда система только создаётся, интеграционные тесты трудно писать. И встаёт дилема: либо пишем костыли и потом их меняем на приличный тестовый код, либо тестируем всё уже в самом конце,


  1. vintage
    05.01.2018 12:56
    +2

    Как-то странно передавать Item в Customer, чтобы создать Order, не находите? Если нам нужен заказ, то его и надо создавать:


    class Order extends Entity {
    
        cost : number
    
        constructor(
            protected customer : ICustomer ,
            protected positions : { product : IProduct , count : number }[] ,
            protected shipper : IShipper ,
        ) {
    
            this.cost = this.positions.reduce(
    
                ( cost , pos )=> price + pos.product.price * pos.count ,
    
                // кидает исключение, если не может доставить
                this.shipper.cost( this.positions , this.customer.region ) ,
            )
    
        }
    
        purchase() {
    
            for( let pos of this.positions ) {
                // кидает исключение, если не может зарезервировать
                pos.product.reserve( pos.count , this.customer )
            }
    
            // кидает исключение, если не может снять денег
            this.customer.charge( this.cost )
        }
    
    }


  1. Mendel
    05.01.2018 12:57
    +1

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

    Принцип единственной ответственности не позволяет так отжигать.
    У нас есть сущности пользователя, товара, счета покупателя, службы доставки, корзины и допустим сделки.
    У клиента есть его география и ссылка на его счет.
    При этом у него может быть очень много ЕГО логики.
    Регистрация, повышение статуса, добавление товара на продажу (методы товара лежат у товара), валидация полей, поздравление пользователя с ДР… все что угодно.
    Но это или чисто его методы или тонкий сахар для доступа к логике связанных данных (например доступ к списку его товаров будет тонким вызовом логики товаров).
    У товара есть куча свойств, включая продавца, цвет коробочки, цена (цены, в зависимости от оптового статуса покупателя и объема конкретной покупки) и конечно список доступных методов доставки. Ну и методы работы со всем этим. Например АПИ вычисления того какая же все-таки тут нужна цена для конкретной сделки.
    Корзина просто содержит товары и их количество. Хотя тут тоже может быть специфическая логика например дергающая резервирование товара на складе и релиз оного резерва при разрушении корзины по таймауту.
    Сделка инкапсулирует в себе данные из корзины (с помощью которой она узнает цены у товаров), доставку (и ее возможность, ведь у нас уже есть и клиент с его адресом, и товар с его адресом и служба доставки с возможными у нее маршрутами) и валидацию доступного баланса у клиента (возможно с резервированием суммы если процесс оформления сделки длительный), методы добавления дополнительной информации и изменения статуса сделки и т.п.
    При этом разумеется каждая ответственность у каждой сущности в отдельном классе расширяющем основной (ну или трейты если есть).
    В таком виде можно и без сервисов довольно приличную логику пилить. При этом все модели толстые, но вся логика легко отделяется друг от друга, юнит-тесты просты и понятны, допиливать и документировать не сложно.