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, должен иметь только одну или две переменные состояния. Редко можно ожидать, что такая тонкая обертка сделает что-нибудь действительно полезное Так что не переусердствуйте с этим.