Каковы практические способы реализации SRP?

11

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

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

Единственный способ, который я нашел, - это использовать предложение "......... должен ......... сам". где первый пробел - это имя класса, а последний - имя метода (ответственности).

Однако иногда трудно понять, действительно ли ответственность нарушает ПСП.

Есть ли еще способы проверить SRP?

Замечания:

Вопрос не в том, что означает SRP, а в практической методологии или серии шагов для проверки и реализации SRP.

ОБНОВИТЬ

Класс отчета

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

Пример отсюда .

Songo
источник
Это интересное правило, но вы все равно можете написать: «Класс Person может отображать себя». Это может считаться нарушением для SRP, поскольку включение графического интерфейса в тот же класс, который содержит бизнес-правила и постоянство данных, не в порядке. Поэтому я думаю, что вам нужно добавить концепцию архитектурных доменов (уровней и уровней) и убедиться, что этот оператор действителен только для 1 из этих доменов (таких как GUI, доступ к данным и т. Д.)
NoChance,
@EmmadKareem Это правило было упомянуто в Head First Object-Oriented Analysis and Design, и это именно то, что я думал об этом. Это несколько не хватает практического способа его реализации. Они отметили, что иногда обязанности не будут такими очевидными для дизайнера, и он должен использовать здравый смысл, чтобы судить, должен ли метод действительно быть в этом классе или нет.
Сонго
Если вы действительно хотите понять SRP, прочитайте некоторые работы дяди Боба Мартина. Его код - один из самых красивых, которые я когда-либо видел, и я верю, что все, что он говорит о SRP, - это не только здравый совет, но и нечто большее, чем просто махать рукой.
Роберт Харви
И объяснит ли избиратель, почему улучшить этот пост ?!
Сонго

Ответы:

7

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

Aaronaught
источник
+1 действительно отличный ответ. Тем не менее, я просто запутался в классе IncomeStatement. Есть ли предлагаемый вами дизайн означает , что IncomeStatementбудет иметь экземпляры IncomeStatementPrinterи IncomeStatementRendererтак , что , когда я звоню print()на IncomeStatementнем будет делегировать вызов IncomeStatementPrinterвместо этого?
Сонго
@ Сонго: Абсолютно нет! У вас не должно быть циклических зависимостей, если вы следуете SOLID. По- видимому , мой ответ не делает его достаточно ясно , что IncomeStatementкласс не имеет в printметод, или formatметод, или любой другой метод , который непосредственно не иметь дело с осмотром или манипулирований самих данных отчета. Вот для чего нужны эти другие классы. Если вы хотите напечатать один, вы берете зависимость от IPrintable<IncomeStatement>интерфейса, который зарегистрирован в контейнере.
Aaronaught
ааа я вижу вашу точку зрения. Однако где циклическая зависимость, если я внедряю Printerэкземпляр в IncomeStatementкласс? способ, которым я воображаю, что это, когда я называю IncomeStatement.print()это, делегирует это IncomeStatementPrinter.print(this, format). Что не так с этим подходом? ... Другой вопрос, который вы упомянули, IncomeStatementдолжен содержать информацию, которая появляется в отформатированных отчетах, если я хочу, чтобы она читалась из базы данных или из файла XML, если мне нужно извлечь метод, который загружает данные в отдельный класс и делегировать вызов в IncomeStatement?
Сонго
@Songo: у вас есть в IncomeStatementPrinterзависимости от IncomeStatementи в IncomeStatementзависимости от IncomeStatementPrinter. Это циклическая зависимость. И это просто плохой дизайн; у вообще нет никаких причин IncomeStatementзнать что-либо о Printerили IncomeStatementPrinter- это модель предметной области, она не связана с печатью, и делегирование бессмысленно, поскольку любой другой класс может создавать или приобретать IncomeStatementPrinter. Нет веской причины иметь какое-либо представление о печати в модели предметной области.
Aaronaught
Что касается того, как вы загружаете IncomeStatementиз базы данных (или файла XML) - обычно это обрабатывается хранилищем и / или картографом, а не доменом, и опять же, вы не делегируете это в домене; если какой-то другой класс должен прочитать одну из этих моделей, он явно запрашивает этот репозиторий . Думаю, если вы не реализуете шаблон Active Record, но я действительно не фанат.
Аарона
2

Чтобы проверить SRP, я проверяю каждый метод (ответственность) класса и задаю следующий вопрос:

«Нужно ли мне когда-либо менять способ реализации этой функции?»

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

Джон Райя
источник
1

Вот цитата из правила 8 предметной гимнастики :

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

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

MattDavey
источник
2
Эта точка зрения безнадежно упрощена. Даже знаменитое, но простое уравнение Эйнштейна требует двух переменных.
Роберт Харви
Вопрос ОП был "Есть ли еще способы проверить SRP?" - это один из возможных показателей. Да, это упрощенно, и это не действует в каждом случае, но это один из возможных способов проверить, был ли нарушен SRP.
MattDavey
1
Я подозреваю, что изменчивое и неизменное состояние также является важным фактором
jk.
Правило 8 описывает идеальный процесс для создания проектов, которые имеют тысячи и тысячи классов, что делает систему безнадежно сложной, непостижимой и не поддерживаемой. Но плюсом является то, что вы можете следовать SRP.
Данк
@ Я не согласен с тобой, но это обсуждение совершенно не по теме.
MattDavey
1

Одна возможная реализация (на Java). Я взял на себя смелость с типами возвращаемых данных, но в целом, я думаю, это отвечает на вопрос. TBH Я не думаю, что интерфейс к классу Report настолько плох, хотя может быть лучше и более подходящее имя. Я упустил охранные заявления и утверждения для краткости.

РЕДАКТИРОВАТЬ: Также обратите внимание, что класс является неизменным. Поэтому, когда он создан, вы ничего не можете изменить. Вы можете добавить setFormatter () и setPrinter () и не столкнуться с большими проблемами. Ключ, IMHO, заключается в том, чтобы не изменять необработанные данные после создания экземпляра.

public class Report
{
    private ReportData data;
    private ReportDataDao dao;
    private ReportFormatter formatter;
    private ReportPrinter printer;


    /*
     *  Parameterized constructor for depndency injection, 
     *  there are better ways but this is explicit.
     */
    public Report(ReportDataDao dao, 
        ReportFormatter formatter, ReportPrinter printer)
    {
        super();
        this.dao = dao;
        this.formatter = formatter;
        this.printer = printer;
    }

    /*
     * Delegates to the injected printer.
     */
    public void printReport()
    {
        printer.print(formatReport());
    }


    /*
     * Lazy loading of data, delegates to the dao 
     * for the meat of the call.
     */
    public ReportData getReportData()
    {
        if (reportData == null)
        {
            reportData = dao.loadData();
        }
        return reportData;
    }

    /*
     * Delegate to the formatter for formatting 
     * (notice a pattern here).
     */
    public ReportData formatReport()
    {
        formatter.format(getReportData());
    }
}
Хит Лилли
источник
Спасибо за реализацию. У меня есть 2 вещи, в строке, if (reportData == null)я полагаю, вы имеете в виду dataвместо этого. Во-вторых, я надеялся узнать, как вы пришли к этой реализации. Например, почему вы решили делегировать все вызовы другим объектам. Еще одна вещь, о которой я всегда задумывался: действительно ли ответственность за то, чтобы печатать отчет? Почему вы не создали отдельный printerкласс, который принимает reportв своем конструкторе?
Сонго
Да, reportData = data, извините за это. Делегирование позволяет детально контролировать зависимости. Во время выполнения вы можете предоставить альтернативные реализации для каждого компонента. Теперь у вас может быть HtmlPrinter, PdfPrinter, JsonPrinter и т. Д. Это также удобно для тестирования, поскольку вы можете тестировать делегированные компоненты изолированно, а также интегрировать в объект выше. Вы, конечно, можете инвертировать отношения между принтером и отчетом, я просто хотел показать, что можно предоставить решение с предоставленным интерфейсом класса. Это привычка работать с устаревшими системами. :)
Хит Лилли,
хмммм ... Итак, если бы вы строили систему с нуля, какой вариант вы бы выбрали? PrinterКласс , который принимает отчет или Reportкласс , который принимает принтер? Я сталкивался с подобной проблемой раньше, когда мне приходилось анализировать отчет, и я спорил с моим TL, должны ли мы создать синтаксический анализатор, который принимает отчет, или же в отчете должен быть синтаксический анализатор и parse()делегирован вызов.
Сонго
Я бы сделал как ... printer.print (report) для запуска и report.print (), если потребуется позже. Самое замечательное в подходе printer.print (report) - его многократное использование. Он разделяет ответственность и позволяет вам использовать удобные методы там, где они вам нужны. Возможно, вы не хотите, чтобы другие объекты в вашей системе знали о ReportPrinter, поэтому, имея метод print () в классе, вы достигаете уровня абстракции, который изолирует логику печати вашего отчета от внешнего мира. Это все еще имеет узкий вектор изменений и прост в использовании.
Хит Лилли
0

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

class Report {
  void format() {
     text = text.trim();
  }

  void print() {
     new Printer().write(text);
  }
}

Методы настолько просты, что не имеет смысла иметь ReportFormatterили ReportPrinterклассы. Единственная вопиющая проблема в интерфейсе - getReportDataэто то, что он нарушает принцип «не говори» для не значащего объекта.

С другой стороны, если методы очень сложны или есть много способов отформатировать или напечатать, Reportтогда имеет смысл делегировать ответственность (также более проверяемое):

class Report {
  void format(ReportFormatter formatter) {
     text = formatter.format(text);
  }

  void print(ReportPrinter printer) {
     printer.write(text);
  }
}

SRP - это принцип проектирования, а не философская концепция, поэтому он основан на реальном коде, с которым вы работаете. Семантически вы можете разделить или сгруппировать класс на столько обязанностей, сколько пожелаете. Однако в качестве практического принципа SRP должен помочь вам найти код, который нужно изменить . Признаки нарушения SRP:

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

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

Гарретт Холл
источник
Не могли бы вы проверить пример, который я приложил к сообщению, и разработать свой ответ на его основе.
Сонго
Обновлено. SRP зависит от контекста, если вы разместите целый класс (в отдельном вопросе), это будет легче объяснить.
Гаррет Холл
Спасибо за обновления. Вопрос, однако, действительно ли ответственность за печать отчета несет ?! Почему вы не создали отдельный класс принтера, который принимает отчет в своем конструкторе?
Сонго
Я просто говорю, что SRP зависит от самого кода, вы не должны применять его догматически.
Гаррет Холл
да, я понимаю вашу точку зрения. Но если бы вы строили систему с нуля, какой вариант вы бы выбрали? PrinterКласс , который принимает отчет или Reportкласс , который принимает принтер? Много раз я сталкивался с таким вопросом проектирования, прежде чем выяснить, окажется ли код сложным или нет.
Сонго
0

Принцип единой ответственности тесно связан с понятием сплоченности . Для того, чтобы иметь очень связный класс, вы должны иметь взаимозависимость между переменными экземпляра класса и его методами; то есть каждый из методов должен манипулировать как можно большим количеством переменных экземпляра. Чем больше переменных использует метод, тем более он связан с его классом; Максимальная сплоченность обычно недостижима.

Кроме того, чтобы хорошо применять SRP, вы хорошо понимаете область бизнес-логики; знать, что должна делать каждая абстракция. Многоуровневая архитектура также связана с SRP, поскольку каждый уровень выполняет определенную функцию (уровень источника данных должен предоставлять данные и т. Д.).

Возвращаясь к сплоченности, даже если ваши методы не используют все переменные, они должны быть связаны:

public class MyClass {
    private Type1 var1;
    private Type2 var2;
    private Type3 var3;

    public Type3 method1() {
        //use var1 and var3
    }  

    public void method2() {
        //use var1 and var2
    }

    public Type1 method3() {
        //use var2 and var3
    }
}

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

public class MyClass {
    private Type1 var1;
    private Type2 var2;
    private Type3 var3;
    private TypeA varA;
    private TypeB varB;

    public Type3 method1() {
        //use var1 and var3
    }  

    public void method2() {
        //use var1 and var2
    }

    public TypeA methodA() {
        //use varA and varB
    }

    public TypeA methodB() {
        //use varA
    }
}
m3th0dman
источник