Принцип единой ответственности против анемического паттерна модели предметной области
Я нахожусь в проекте, который довольно серьезно относится к принципу единой ответственности. У нас много маленьких классов, и все довольно просто. Тем не менее, у нас есть модель анемичной области - в любом из наших классов моделей нет поведения, это просто пакеты свойств. Это не претензия к нашему дизайну - на самом деле кажется, что он работает довольно хорошо
Во время проверки проекта SRP выявляется всякий раз, когда в систему добавляется новое поведение, и поэтому новое поведение обычно заканчивается в новом классе. Это позволяет легко проверять все на единицу, но я иногда недоумеваю, потому что это похоже на то, чтобы вытащить поведение из того места, где оно уместно.
Я пытаюсь лучше понять, как правильно применять SRP. Мне кажется, что SRP выступает против добавления поведения бизнес-моделирования, которое разделяет один и тот же контекст, к одному объекту, потому что объект неизбежно заканчивает тем, что делает больше, чем одну связанную вещь, или делает одну вещь, но зная множество бизнес-правил, которые меняют форму его выходов.
Если это так, то кажется, что конечным результатом является модель анемичной области, что, безусловно, имеет место в нашем проекте. Тем не менее, модель анемичной области является анти-паттерном.
Могут ли эти две идеи сосуществовать?
РЕДАКТИРОВАТЬ: пару контекстных ссылок:
SRP - http://www.objectmentor.com/resources/articles/srp.pdf
Модель анемичного домена - http://martinfowler.com/bliki/AnemicDomainModel.html
Я не тот разработчик, которому просто нравится искать пророка и следовать тому, что они говорят как Евангелие. Поэтому я не даю ссылки на них как способ обозначения "это правила", просто как источник определения этих двух понятий.
7 ответов
Я должен был сказать "да", но вы должны сделать свой SRP правильно. Если та же самая операция применяется только к одному классу, она принадлежит этому классу, не так ли? Как насчет, если одна и та же операция применяется к нескольким классам? В этом случае, если вы хотите следовать ОО-модели объединения данных и поведения, вы бы поместили операцию в базовый класс, не так ли?
Я подозреваю, что из вашего описания вы в конечном итоге получаете классы, которые в основном представляют собой пакеты операций, поэтому вы по существу воссоздали C-стиль кодирования: структуры и модули.
Из связанной статьи SRP: "SRP - один из самых простых принципов и один из самых трудных для понимания".
Богатая модель предметной области (RDM) и принцип единой ответственности (SRP) не обязательно противоречат друг другу. RDM в большей степени расходится с очень специализированным подклассом SRP - моделью, пропагандирующей "фасоли данных + вся бизнес-логика в классах контроллеров" (DBABLICC).
Если вы прочтете главу SRP Мартина, вы увидите, что его пример с модемом целиком находится на уровне домена, но абстрагирует понятия DataChannel и Connection в отдельные классы. Он хранит сам модем как обертку, поскольку это полезная абстракция для клиентского кода. Это гораздо больше о правильном (ре) факторинге, чем просто наслоение. Сплоченность и сцепление остаются базовыми принципами дизайна.
Наконец, три вопроса:
Как отмечает сам Мартин, не всегда легко увидеть разные "причины перемен". Сами концепции YAGNI, Agile и т. Д. Не позволяют предвидеть будущие причины перемен, поэтому мы не должны придумывать те, в которых они не очевидны сразу. Я вижу "преждевременные, ожидаемые причины изменений" как реальный риск при применении SRP, и разработчик должен управлять ими.
В дополнение к предыдущему, даже правильное (но ненужное анальное) применение SRP может привести к нежелательной сложности. Всегда думайте о следующем бедняге, который должен поддерживать ваш класс: поможет ли усердная абстракция тривиального поведения в его собственные интерфейсы, базовые классы и однострочные реализации его пониманию того, что просто должен был быть один класс?
Дизайн программного обеспечения часто заключается в достижении наилучшего компромисса между конкурирующими силами. Например, многоуровневая архитектура в основном является хорошим приложением SRP, но как насчет того, что, например, изменение свойства бизнес-класса с, скажем, логического значения на перечисление, имеет волновой эффект на всех уровнях - от БД через домен, фасады, веб-сервис, до GUI? Это указывает на плохой дизайн? Не обязательно: это указывает на тот факт, что ваш дизайн предпочитает один аспект изменения другому.
Цитата из статьи SRP очень правильная; SRP трудно понять правильно. Этот и OCP являются двумя элементами SOLID, которые просто должны быть смягчены, по крайней мере, до некоторой степени, чтобы фактически выполнить проект. Чрезмерное применение любого из них очень быстро произведет код равиоли.
SRP действительно можно принять до смешного, если "причины изменений" слишком конкретны. Даже "пакет данных" POCO/POJO может рассматриваться как нарушающий SRP, если вы рассматриваете тип поля, изменяющегося как "изменение". Вы могли бы подумать, что здравый смысл скажет вам, что изменение типа поля является необходимым условием для "изменения", но я видел доменные слои с обертками для встроенных типов значений; ад, который делает ADM похожим на утопию.
Часто бывает полезно поставить перед собой какую-то реалистичную цель, основанную на удобочитаемости или желаемом уровне сплоченности. Когда вы говорите: "Я хочу, чтобы этот класс делал одно", он должен иметь не больше или меньше того, что необходимо для этого. Вы можете поддерживать, по крайней мере, процедурное единство с этой основной философией. "Я хочу, чтобы этот класс поддерживал все данные для счета-фактуры", как правило, допускает некоторую бизнес-логику, даже суммируя промежуточные итоги или рассчитывая налог с продаж, исходя из ответственности объекта знать, как дать вам точное, внутренне согласованное значение для любого поля. это содержит.
У меня лично нет большой проблемы с "легким" доменом. Простая роль "эксперта по данным" делает объект домена хранителем каждого поля / свойства, относящегося к классу, а также всей вычисляемой логики поля, любых явных / неявных преобразований типов данных и, возможно, более простых правил проверки. (т.е. обязательные поля, пределы значений, вещи, которые могли бы разрушить экземпляр внутри, если это разрешено) Если алгоритм вычисления, возможно, для взвешенного или скользящего среднего, вероятно, изменится, инкапсулируйте алгоритм и обратитесь к нему в вычисляемом поле (это просто хороший OCP/PV).
Я не считаю такой предметный объект "анемичным". Мое восприятие этого термина - "пакет данных", набор полей, которые не имеют никакого понятия о внешнем мире или даже о взаимосвязи между его полями, отличной от того, что он их содержит. Я тоже это видел, и не очень интересно отслеживать несоответствия в состоянии объекта, которые объект никогда не знал, было проблемой. Чрезмерно усердный SRP приведет к этому, заявив, что объект данных не отвечает за какую-либо бизнес-логику, но обычно сначала вмешивается здравый смысл и говорит, что объект, как эксперт по данным, должен отвечать за поддержание согласованного внутреннего состояния.
Опять же, личное мнение, я предпочитаю шаблон Repository Active Record. Один объект, с одной ответственностью, и очень мало, если что-либо еще в системе выше этого уровня, должен знать что-либо о том, как он работает. Active Record требует, чтобы доменный уровень знал, по крайней мере, некоторые конкретные детали о методе или структуре персистентности (будь то имена хранимых процедур, используемых для чтения / записи каждого класса, специфические для платформы ссылки на объекты или атрибуты, украшающие поля информацией ORM)) и, таким образом, вводит вторую причину для перехода в каждый класс домена по умолчанию.
Мои $0,02.
Я обнаружил, что следование твердым принципам на самом деле уводит меня от модели богатых доменов DDD, в конце концов я обнаружил, что мне все равно. Более того, я обнаружил, что логическая концепция модели предметной области и класса на любом языке не была отображена 1:1, если мы не говорили о каком-либо фасаде.
Я бы не сказал, что это именно c-стиль программирования, в котором у вас есть структуры и модули, но скорее вы, вероятно, получите что-то более функциональное, я понимаю, что стили похожи, но детали имеют большое значение. Я обнаружил, что экземпляры моего класса ведут себя как функции высшего порядка, приложения с частичными функциями, лениво вычисляемые функции или некоторая комбинация вышеперечисленного. Это несколько невыразимо для меня, но такое чувство я получаю от написания кода после TDD + SOLID, в конечном итоге он ведет себя как гибридный OO/ функциональный стиль.
Что касается наследования, являющегося плохим словом, я думаю, что это больше из-за того факта, что наследование недостаточно детализировано в таких языках, как Java/C#. На других языках это менее важно и более полезно.
Мне нравится определение SRP как:
"У класса есть только одна бизнес-причина для изменения"
Таким образом, до тех пор, пока поведение может быть сгруппировано в единые "деловые причины", у них нет причин не сосуществовать в одном классе. Конечно, то, что определяет "деловую причину", открыто для обсуждения (и должно обсуждаться всеми заинтересованными сторонами).
Прежде чем я приступлю к своей напыщенной речи, вот мое мнение в двух словах: где-то все должно собраться вместе... и затем река протекает через него.
Меня преследует кодирование.
=======
Анемичная модель данных и я... ну, мы много общаемся. Может быть, это просто природа приложений малого и среднего размера с очень небольшой встроенной бизнес-логикой. Может быть, я немного запутался.
Тем не менее, вот мои 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.
Преобразуйте ваши простые доменные объекты в шаблон ActiveRecord с общим базовым классом для всех доменных объектов. Поместите общее поведение в базовый класс и переопределите поведение в производных классах, где это необходимо, или определите новое поведение, где это необходимо.