Принцип единой ответственности против анемического паттерна модели предметной области

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

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

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

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

Могут ли эти две идеи сосуществовать?

РЕДАКТИРОВАТЬ: несколько контекстных ссылок:

SRP - http://www.objectmentor.com/resources/articles/srp.pdf
Модель анемичного домена - http://martinfowler.com/bliki/AnemicDomainModel.html

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

Вы многому научились с тех пор, как опубликовали это? У меня есть опыт разработки в течение нескольких лет, и я нахожусь в похожей ситуации, которую вы описали, когда я вижу проект, демонстрирующий признаки модели Anemic Domain, но я не уверен, как сбалансировать OOD с SRP.
CamHart 2.02.2018 00:37:44
Вау, это было давно. Я думаю, что это отражает то, куда ушел ОО - долгий путь от того, с чего все началось. В колледже (давным-давно) я узнал, что ОО касалось инкапсуляции данных с соответствующими действиями в классах и обмена поведением с помощью наследования. Теперь это легкие классы данных с другими классами для работы с ними, склеенные посредством шаблонов проектирования и внедрения зависимостей, а наследование - это четырехбуквенное слово. SRP работает намного лучше с последним стилем OO, чем с первым.
Niall Connaughton 2.02.2018 02:25:52
Что я узнал? Первоначальная идея моделирования сущностей как богатых классов может привести к путанице иерархий и дыр, пробитых в инкапсуляции, чтобы позволить модели сгибаться по-новому. Это не очень хорошо работает с TDD, который частично использовал SRP и DI. В основном это нормально. Мы потеряли интуитивно понятные места в коде, где вы можете посмотреть, как ваша система работает. Теперь вы должны знать все задействованные классы актеров или сканировать кодовую базу. Мы полагаемся на IDE, чтобы найти способы использования, реализации интерфейсов и т. Д. Это отчасти способы справиться с тем, что принесли нам SRP и DI.
Niall Connaughton 2.02.2018 02:30:18
На высоком уровне, переходя от моделей с расширенным доменом к SRP, мы поменяли чистые модели, которые превращаются в монстров, которых трудно разделить, на чистые наборы небольших классов, которые в конечном итоге превращаются в беспорядок, который никто не может полностью отследить. Модели с богатым доменом заманчивы, особенно на ранних этапах, но в конечном итоге вы обнаружите, что никогда не сможете идеально смоделировать проблему, и она может быстро выйти из строя. Подход SRP, вероятно, более гибок в долгосрочной перспективе, но ни один из них не подарит вам пожизненное счастье.
Niall Connaughton 2.02.2018 02:38:14
Спасибо, что так быстро вернулся. Похоже, там нет серебряной пули. Я ценю компромиссы, на которые вы указываете.
CamHart 2.02.2018 17:30:55
7 ОТВЕТОВ
РЕШЕНИЕ

Я бы сказал «да», но вы должны правильно выполнить свой SRP. Если та же самая операция применяется только к одному классу, она принадлежит этому классу, не так ли? Как насчет того, если одна и та же операция применяется к нескольким классам? В этом случае, если вы хотите следовать ОО-модели комбинирования данных и поведения, вы бы поместили операцию в базовый класс, не так ли?

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

Из связанной статьи SRP: « SRP - один из самых простых принципов и один из самых трудных для понимания ».

9
9.09.2009 11:42:14
Хм ... Я бы хотел убедиться, но это не так. Композиция в пользу наследования говорит против вашего аргумента ...
Mark Seemann 9.09.2009 13:49:06
Хорошо, мы можем обмениваться цитатами из Священных Писаний или спорить о том, требует ли «одолжение» абсолютизма. Или мы можем попытаться решить проблему ОП. Что вы думаете о том, как решить проблему ОП?
CPerkins 9.09.2009 14:55:31
Спасибо за Ваш ответ. Я нахожу в эти дни, что ОО модель комбинирования данных и поведения очень не в моде. Я, конечно, знаю о подводных камнях, которые могут возникнуть в сложных иерархиях наследования, но в наши дни наследование - это почти запретное слово. SRP и предпочтение композиции перед наследованием, кажется, толкают против моделей с богатой областью. Я думаю, что проблемы возникают, когда принципы соблюдаются, потому что они являются принципами, а не потому, что они лучше всего работают для вашего проекта. Как вы говорите, цитаты из Священных Писаний.
Niall Connaughton 10.09.2009 09:46:31
Ваше предпоследнее предложение очень мудро, но оно поставит вас в противоречие со многими людьми. Идеология проще, чем анализ.
CPerkins 10.09.2009 11:37:12
@CPerkins: Я следил за этим вопросом, потому что я искренне заинтересован в том, чтобы получить ответ сам - у меня есть почти такой же опыт, что композиция по наследованию имеет тенденцию делать доменные модели более анемичными. Просто у композиции есть много других преимуществ, поэтому я не хочу моделировать логику по наследству, если это не имеет смысла. Если бы у меня был лучший ответ, я бы опубликовал его. Мне бы очень хотелось получить убедительный ответ, но я думаю, что мне нужно будет продолжать исследовать это профессионально самостоятельно. Это не имеет ничего общего с «писанием».
Mark Seemann 14.09.2009 05:45:44

Я обнаружил, что следование твердым принципам на самом деле уводит меня от модели богатых доменов DDD, в конце концов я обнаружил, что мне все равно. Более того, я обнаружил, что логическая концепция модели предметной области и класса на каком-либо языке не отображалась 1: 1, если мы не говорили о каком-либо фасаде.

Я бы не сказал, что это именно c-стиль программирования, где у вас есть структуры и модули, но скорее вы, вероятно, в итоге получите что-то более функциональное, я понимаю, что стили похожи, но детали имеют большое значение. Я обнаружил, что экземпляры моего класса ведут себя как функции более высокого порядка, приложения с частичными функциями, лениво вычисляемые функции или некоторая комбинация вышеперечисленного. Это несколько невыразимо для меня, но такое чувство я получаю от написания кода после TDD + SOLID, в итоге он ведет себя как гибридный OO / функциональный стиль.

Что касается наследования, являющегося плохим словом, я думаю, что это больше из-за того факта, что наследование недостаточно детализировано в таких языках, как Java / C #. На других языках это менее важно и более полезно.

4
14.09.2009 20:38:21

Мне нравится определение SRP как:

«У класса есть только одна бизнес-причина для изменения»

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

1
29.10.2009 20:12:47

Богатая модель предметной области (RDM) и принцип единой ответственности (SRP) не обязательно расходятся. RDM в большей степени расходится с очень специализированным подклассом SRP - моделью, пропагандирующей «фасоли данных + вся бизнес-логика в классах контроллеров» (DBABLICC).

Если вы прочтете главу SRP Мартина , вы увидите, что его пример с модемом целиком находится на уровне домена, но абстрагирует понятия DataChannel и Connection в отдельные классы. Он хранит сам модем как обертку, поскольку это полезная абстракция для клиентского кода. Это гораздо больше о правильном (пере) факторинге, чем просто наслоение . Сплоченность и сцепление остаются базовыми принципами дизайна.

Наконец, три вопроса:

  • Как отмечает сам Мартин, не всегда легко увидеть разные «причины перемен». Сами концепции YAGNI, Agile и т. Д. Не позволяют предвидеть будущие причины перемен, поэтому мы не должны придумывать те, в которых они не очевидны сразу. Я вижу «преждевременные, ожидаемые причины изменений» как реальный риск при применении SRP, и разработчик должен управлять ими.

  • В дополнение к предыдущему, даже правильное (но ненужное анальное ) применение SRP может привести к нежелательной сложности. Всегда думайте о следующем бедняге, который должен поддерживать ваш класс: поможет ли усердная абстракция тривиального поведения в его собственные интерфейсы, базовые классы и однострочные реализации помочь его пониманию того, что просто должен был быть один класс?

  • Дизайн программного обеспечения часто заключается в достижении наилучшего компромисса между конкурирующими силами. Например, многоуровневая архитектура в основном является хорошим приложением SRP, но как насчет того, что, например, изменение свойства бизнес-класса с, скажем, логического значения на перечисление, имеет волновой эффект на всех уровнях - от БД через домен, фасады, веб-сервис, до GUI? Это указывает на плохой дизайн? Не обязательно: это указывает на тот факт, что ваш дизайн предпочитает один аспект изменения другому.

13
2.03.2020 18:41:18
«DBABLICC» на самом деле не форма SRP, а процедурный программист, скорее способ рационализировать свое процедурное программирование с использованием языка ООП.
Sled 14.10.2011 17:13:20

Преобразуйте ваши простые доменные объекты в шаблон ActiveRecord с общим базовым классом для всех доменных объектов. Поместите общее поведение в базовый класс и переопределите поведение в производных классах, где это необходимо, или определите новое поведение, где это необходимо.

0
3.06.2010 08:34:42

Цитата из статьи SRP очень правильная; SRP трудно понять правильно. Этот и OCP - это два элемента SOLID, которые просто необходимо ослабить хотя бы до некоторой степени, чтобы фактически выполнить проект. Чрезмерное применение любого из них очень быстро произведет код равиоли.

SRP действительно можно принять до смешного, если «причины изменений» слишком конкретны. Даже «пакет данных» POCO / POJO может рассматриваться как нарушающий SRP, если вы рассматриваете тип поля, изменяющегося как «изменение». Вы могли бы подумать, что здравый смысл скажет вам, что изменение типа поля является необходимым условием для «изменения», но я видел доменные слои с обертками для встроенных типов значений; ад, который делает ADM похожим на утопию.

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

У меня лично нет большой проблемы с «легким» доменом. Одна только роль «эксперта по данным» делает объект домена хранителем каждого поля / свойства, относящегося к классу, а также всей вычисляемой логики поля, любых явных / неявных преобразований типов данных и, возможно, более простых правил проверки. (т. е. обязательные поля, пределы значений, вещи, которые внутренне сломали бы экземпляр, если позволили) Если алгоритм вычисления, возможно, для взвешенного или скользящего среднего, вероятно, изменится, инкапсулируйте алгоритм и обратитесь к нему в вычисляемом поле (это просто хороший OCP / PV).

Я не считаю такой предметный объект «анемичным». Мое восприятие этого термина - «пакет данных», набор полей, которые не имеют никакого понятия о внешнем мире или даже о взаимосвязи между его полями, отличной от того, что он их содержит. Я тоже это видел, и неинтересно отслеживать несоответствия в состоянии объекта, которые объект никогда не знал, было проблемой. Чрезмерно усердный SRP приведет к этому, заявив, что объект данных не отвечает за какую-либо бизнес-логику, но здравый смысл обычно вмешивается в первую очередь и говорит, что объект, как эксперт по данным, должен отвечать за поддержание согласованного внутреннего состояния.

Опять же, личное мнение, я предпочитаю шаблон Repository Active Record. Один объект, с одной ответственностью, и очень мало, если что-либо еще в системе выше этого уровня, должен знать что-либо о том, как он работает. Active Record требует, чтобы уровень домена знал, по крайней мере, некоторые специфические детали о методе или структуре персистентности (будь то имена хранимых процедур, используемых для чтения / записи каждого класса, специфические для платформы ссылки на объекты или атрибуты, украшающие поля информацией ORM) ) и, таким образом, вводит вторую причину для перехода в каждый класс домена по умолчанию.

Мои $ 0,02.

7
31.08.2010 15:56:41
Я думаю, что ваш комментарий ближе к концу, что объект должен отвечать за поддержание согласованного внутреннего состояния, является настоящим решающим фактором. Анемичные модели, которые я видел, оставляют это классам, которые манипулируют моделью через общедоступные сеттеры, основываясь на идее, что вычисление значения - это другая ответственность за предоставление значения, независимо от сложности вычисления.
Niall Connaughton 1.09.2010 10:51:36

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

Меня преследует кодирование.

=======

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

Тем не менее, вот мои 2 цента:

Не могли бы вы просто выделить код в сущностях и привязать его к интерфейсу?

public class Object1
{
    public string Property1 { get; set; }
    public string Property2 { get; set; }

    private IAction1 action1;

    public Object1(IAction1 action1)
    {
        this.action1 = action1;
    }

    public void DoAction1()
    {
        action1.Do(Property1);
    }
}

public interface IAction1
{
    void Do(string input1);
}

Это как-то нарушает принципы ПСП?

Кроме того, разве куча классов, сидящих без дела, не привязанных друг к другу ничем, кроме потребляющего кода, на самом деле является большим нарушением SRP, но подталкивает уровень?

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

Кроме того, вы могли бы сказать: «Нет, брат, все, что ему нужно сделать, - это получить доступ к уровню обслуживания. Это похоже на Object1Service.DoActionX (Object1). Кусок пирога». Тогда где же логика? Все в этом одном методе? Вы все еще просто толкаете код, и, несмотря ни на что, вы в конечном итоге будете отделять данные и логику.

Итак, в этом сценарии, почему бы не предоставить клиентскому коду этот конкретный Object1Service и сделать его DoActionX () в основном просто еще одним хуком для вашей доменной модели? Под этим я подразумеваю:

public class Object1Service
{
    private Object1Repository repository;

    public  Object1Service(Object1Repository repository)
    {
        this.repository = repository;
    }

    // Tie in your Unit of Work Aspect'ing stuff or whatever if need be
    public void DoAction1(Object1DTO object1DTO)
    {
        Object1 object1 = repository.GetById(object1DTO.Id);
        object1.DoAction1();
        repository.Save(object1);
    }
}

Вы все еще разобрали фактический код для Action1 из Object1, но для всех интенсивных целей имеете неанемичный Object1.

Скажем, вам нужен Action1 для представления 2 (или более) различных операций, которые вы хотели бы сделать атомарными и разделенными на их собственные классы. Просто создайте интерфейс для каждой атомарной операции и подключите его внутри DoAction1.

Вот как я мог бы подойти к этой ситуации. Но опять же, я действительно не знаю, что такое SRP.

1
14.03.2012 23:13:20