Просто какие практические методы люди используют, чтобы проверить, нарушает ли класс принцип единственной ответственности?
Я знаю, что у класса должна быть только одна причина для изменения, но этому предложению не хватает практического способа действительно реализовать это.
Единственный способ, который я нашел, - это использовать предложение "......... должен ......... сам". где первый пробел - это имя класса, а последний - имя метода (ответственности).
Однако иногда трудно понять, действительно ли ответственность нарушает ПСП.
Есть ли еще способы проверить SRP?
Замечания:
Вопрос не в том, что означает SRP, а в практической методологии или серии шагов для проверки и реализации SRP.
ОБНОВИТЬ
Я добавил образец класса, который явно нарушает SRP. Было бы здорово, если бы люди могли использовать это в качестве примера, чтобы объяснить, как они подходят к принципу единой ответственности.
Пример отсюда .
Ответы:
SRP безоговорочно заявляет, что у класса должна быть только одна причина для изменения.
Деконструируя класс «report» в вопросе, он имеет три метода:
printReport
getReportData
formatReport
Игнорируя избыточность
Report
, используемую в каждом методе, легко понять, почему это нарушает SRP:Термин «печать» подразумевает некоторый интерфейс или реальный принтер. Поэтому этот класс содержит некоторое количество пользовательского интерфейса или логики представления. Изменение требований пользовательского интерфейса потребует изменения
Report
класса.Термин «данные» подразумевает некоторую структуру данных, но не определяет, что именно (XML? JSON? CSV?). В любом случае, если «содержание» отчета когда-либо изменится, то и этот метод изменится. Существует связь с базой данных или доменом.
formatReport
это просто ужасное название для метода в целом, но я бы предположил, посмотрев на него, что он снова имеет какое-то отношение к пользовательскому интерфейсу, и, вероятно, отличается от негоprintReport
. Итак, еще одна, не связанная причина, чтобы измениться.Таким образом, этот один класс, возможно, связан с базой данных, устройством экрана / принтера и некоторой внутренней логикой форматирования для журналов или вывода файлов или чего-либо еще. Имея все три функции в одном классе, вы умножаете количество зависимостей и увеличиваете в три раза вероятность того, что любое изменение зависимости или требования нарушит этот класс (или что-то еще, что зависит от него).
Частично проблема в том, что вы выбрали особенно сложный пример. Вероятно, вам не следует называть класс
Report
, даже если он делает только одно , потому что ... какой отчет? Разве не все «отчеты» - это совершенно разные звери, основанные на разных данных и разных требованиях? И не является ли отчет чем-то уже отформатированным, ни для экрана, ни для печати?Но, оглянувшись на это и составив гипотетическое конкретное имя - давайте назовем его
IncomeStatement
(один очень распространенный отчет) - правильная архитектура «SRPed» будет иметь три типа:IncomeStatement
- класс домена и / или модели, который содержит и / или вычисляет информацию, которая появляется в отформатированных отчетах.IncomeStatementPrinter
, что, вероятно, будет реализовывать какой-то стандартный интерфейс, какIPrintable<T>
. Имеет один ключевой методPrint(IncomeStatement)
и, возможно, некоторые другие методы или свойства для настройки параметров печати.IncomeStatementRenderer
, который обрабатывает отображение экрана и очень похож на класс принтера.Вы также можете в конечном итоге добавить больше специфических классов, таких как
IncomeStatementExporter
/IExportable<TReport, TFormat>
.Это значительно упрощается в современных языках благодаря введению обобщений и контейнеров IoC. Большая часть кода вашего приложения не должна полагаться на определенный
IncomeStatementPrinter
класс, он может использоватьIPrintable<T>
и, следовательно, работать с любым типом печатного отчета, который дает вам все ощутимые преимуществаReport
базового класса с помощьюprint
метода и ни одного из обычных нарушений SRP. , Фактическая реализация должна быть объявлена только один раз при регистрации контейнера IoC.Некоторые люди, сталкиваясь с вышеуказанным дизайном, отвечают примерно так: «но это похоже на процедурный код, и весь смысл ООП заключался в том, чтобы избавить нас от разделения данных и поведения!» На что я говорю: неправильно .
IncomeStatement
Это не только «данные», и вышеупомянутая ошибка состоит в том, что вызывает много OOP людей , чтобы чувствовать , что они делают что - то неправильны, создавая такой «прозрачный» класс , а затем начать глушение всех видов несвязанных функциональной группы вIncomeStatement
(ну, и вообще лень). Этот класс может начинаться как просто данные, но со временем гарантированно он станет моделью .Например, отчет о реальном доходе имеет общие доходы , общие расходы и строки чистого дохода . Правильно спроектированная финансовая система, скорее всего, не будет хранить их, поскольку они не являются транзакционными данными - фактически они изменяются в зависимости от добавления новых транзакционных данных. Однако вычисление этих строк всегда будет одинаковым, независимо от того, печатаете ли вы отчет или экспортируете его. Так что ваш
IncomeStatement
класс будет иметь величину справедливого поведения к нему в формеgetTotalRevenues()
,getTotalExpenses()
иgetNetIncome()
методы, и , возможно , некоторые другие. Это подлинный объект в стиле ООП со своим собственным поведением, даже если кажется, что он на самом деле мало «делает».Но
format
иprint
методы, они не имеют ничего общего с самой информацией. На самом деле, не исключено, что вам понадобится несколько реализаций этих методов, например, подробное заявление для руководства и не очень подробное заявление для акционеров. Разделение этих независимых функций на разные классы дает вам возможность выбирать разные реализации во время выполнения без бремени метода «один размер подходит всем»print(bool includeDetails, bool includeSubtotals, bool includeTotals, int columnWidth, CompanyLetterhead letterhead, ...)
. Тьфу!Надеемся, что вы можете увидеть, где вышеописанный метод с массивной параметризацией работает неправильно и где отдельные реализации работают правильно; в случае с одним объектом, каждый раз, когда вы добавляете новую складку в логику печати, вы должны изменить модель своего домена ( Тиму в финансах нужны номера страниц, но вы можете добавить это только во внутренний отчет? ), в отличие от вместо этого просто добавив свойство конфигурации к одному или двум сателлитным классам.
Правильная реализация SRP - это управление зависимостями . Короче говоря, если класс уже делает что-то полезное, и вы рассматриваете возможность добавления другого метода, который бы вводил новую зависимость (например, пользовательский интерфейс, принтер, сеть, файл и т. Д.), Не делайте этого . Подумайте, как вы могли бы добавить эту функциональность вместо этого в новый класс, и как вы могли бы сделать этот новый класс вписывающимся в вашу общую архитектуру (это довольно легко, когда вы проектируете вокруг внедрения зависимостей). Это общий принцип / процесс.
Примечание: Как и Роберт, я явно отвергаю идею о том, что класс, соответствующий SRP, должен иметь только одну или две переменные состояния. Редко можно ожидать, что такая тонкая обертка сделает что-нибудь действительно полезное Так что не переусердствуйте с этим.
источник
IncomeStatement
. Есть ли предлагаемый вами дизайн означает , чтоIncomeStatement
будет иметь экземплярыIncomeStatementPrinter
иIncomeStatementRenderer
так , что , когда я звонюprint()
наIncomeStatement
нем будет делегировать вызовIncomeStatementPrinter
вместо этого?IncomeStatement
класс не имеет вprint
метод, илиformat
метод, или любой другой метод , который непосредственно не иметь дело с осмотром или манипулирований самих данных отчета. Вот для чего нужны эти другие классы. Если вы хотите напечатать один, вы берете зависимость отIPrintable<IncomeStatement>
интерфейса, который зарегистрирован в контейнере.Printer
экземпляр вIncomeStatement
класс? способ, которым я воображаю, что это, когда я называюIncomeStatement.print()
это, делегирует этоIncomeStatementPrinter.print(this, format)
. Что не так с этим подходом? ... Другой вопрос, который вы упомянули,IncomeStatement
должен содержать информацию, которая появляется в отформатированных отчетах, если я хочу, чтобы она читалась из базы данных или из файла XML, если мне нужно извлечь метод, который загружает данные в отдельный класс и делегировать вызов вIncomeStatement
?IncomeStatementPrinter
зависимости отIncomeStatement
и вIncomeStatement
зависимости отIncomeStatementPrinter
. Это циклическая зависимость. И это просто плохой дизайн; у вообще нет никаких причинIncomeStatement
знать что-либо оPrinter
илиIncomeStatementPrinter
- это модель предметной области, она не связана с печатью, и делегирование бессмысленно, поскольку любой другой класс может создавать или приобретатьIncomeStatementPrinter
. Нет веской причины иметь какое-либо представление о печати в модели предметной области.IncomeStatement
из базы данных (или файла XML) - обычно это обрабатывается хранилищем и / или картографом, а не доменом, и опять же, вы не делегируете это в домене; если какой-то другой класс должен прочитать одну из этих моделей, он явно запрашивает этот репозиторий . Думаю, если вы не реализуете шаблон Active Record, но я действительно не фанат.Чтобы проверить SRP, я проверяю каждый метод (ответственность) класса и задаю следующий вопрос:
«Нужно ли мне когда-либо менять способ реализации этой функции?»
Если я найду функцию, которую мне нужно будет реализовать различными способами (в зависимости от какой-либо конфигурации или условия), то я точно знаю, что мне нужен дополнительный класс для выполнения этой обязанности.
источник
Вот цитата из правила 8 предметной гимнастики :
Учитывая эту (несколько идеалистическую) точку зрения, вы можете сказать, что любой класс, содержащий только одну или две переменные состояния, вряд ли нарушит SRP. Можно также сказать, что любой класс, содержащий более двух переменных состояния, может нарушать SRP.
источник
Одна возможная реализация (на Java). Я взял на себя смелость с типами возвращаемых данных, но в целом, я думаю, это отвечает на вопрос. TBH Я не думаю, что интерфейс к классу Report настолько плох, хотя может быть лучше и более подходящее имя. Я упустил охранные заявления и утверждения для краткости.
РЕДАКТИРОВАТЬ: Также обратите внимание, что класс является неизменным. Поэтому, когда он создан, вы ничего не можете изменить. Вы можете добавить setFormatter () и setPrinter () и не столкнуться с большими проблемами. Ключ, IMHO, заключается в том, чтобы не изменять необработанные данные после создания экземпляра.
источник
if (reportData == null)
я полагаю, вы имеете в видуdata
вместо этого. Во-вторых, я надеялся узнать, как вы пришли к этой реализации. Например, почему вы решили делегировать все вызовы другим объектам. Еще одна вещь, о которой я всегда задумывался: действительно ли ответственность за то, чтобы печатать отчет? Почему вы не создали отдельныйprinter
класс, который принимаетreport
в своем конструкторе?Printer
Класс , который принимает отчет илиReport
класс , который принимает принтер? Я сталкивался с подобной проблемой раньше, когда мне приходилось анализировать отчет, и я спорил с моим TL, должны ли мы создать синтаксический анализатор, который принимает отчет, или же в отчете должен быть синтаксический анализатор иparse()
делегирован вызов.В вашем примере не ясно, нарушается ли SRP. Возможно, отчет должен быть в состоянии отформатировать и распечатать сам, если они относительно просты:
Методы настолько просты, что не имеет смысла иметь
ReportFormatter
илиReportPrinter
классы. Единственная вопиющая проблема в интерфейсе -getReportData
это то, что он нарушает принцип «не говори» для не значащего объекта.С другой стороны, если методы очень сложны или есть много способов отформатировать или напечатать,
Report
тогда имеет смысл делегировать ответственность (также более проверяемое):SRP - это принцип проектирования, а не философская концепция, поэтому он основан на реальном коде, с которым вы работаете. Семантически вы можете разделить или сгруппировать класс на столько обязанностей, сколько пожелаете. Однако в качестве практического принципа SRP должен помочь вам найти код, который нужно изменить . Признаки нарушения SRP:
Вы можете исправить это с помощью рефакторинга, улучшив имена, сгруппировав похожий код, исключив дублирование, используя многоуровневый дизайн и разделив / объединив классы по мере необходимости. Лучший способ выучить SRP - это погрузиться в кодовую базу и устранить рефакторинг.
источник
Printer
Класс , который принимает отчет илиReport
класс , который принимает принтер? Много раз я сталкивался с таким вопросом проектирования, прежде чем выяснить, окажется ли код сложным или нет.Принцип единой ответственности тесно связан с понятием сплоченности . Для того, чтобы иметь очень связный класс, вы должны иметь взаимозависимость между переменными экземпляра класса и его методами; то есть каждый из методов должен манипулировать как можно большим количеством переменных экземпляра. Чем больше переменных использует метод, тем более он связан с его классом; Максимальная сплоченность обычно недостижима.
Кроме того, чтобы хорошо применять SRP, вы хорошо понимаете область бизнес-логики; знать, что должна делать каждая абстракция. Многоуровневая архитектура также связана с SRP, поскольку каждый уровень выполняет определенную функцию (уровень источника данных должен предоставлять данные и т. Д.).
Возвращаясь к сплоченности, даже если ваши методы не используют все переменные, они должны быть связаны:
Вы не должны иметь что-то вроде приведенного ниже кода, где часть переменных экземпляра используется в части методов, а другая часть переменных используется в другой части методов (здесь у вас должно быть два класса для каждая часть переменных).
источник