Как класс может иметь несколько методов, не нарушая принцип единой ответственности

64

Принцип единой ответственности определяется в Википедии как

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

Если у класса должна быть только одна ответственность, как он может иметь более одного метода? Не будет ли у каждого метода разная ответственность, что будет означать, что у класса будет более 1 ответственности.

Каждый пример, который я видел, демонстрируя принцип единственной ответственности, использует пример класса, который имеет только один метод. Это может помочь увидеть пример или получить объяснение класса с несколькими методами, которые все еще можно считать ответственными.

Гусь
источник
11
Почему отрицательный голос? Похоже, идеальный вопрос для SE.SE; человек исследовал тему и приложил усилия, чтобы сделать вопрос очень ясным. Это заслуживает голосов против.
Арсений Мурзенко
19
Вероятно, это было связано с тем, что этот вопрос задавался и на него уже отвечали несколько раз, например, см. Softwareengineering.stackexchange.com/questions/345018/… . На мой взгляд, это не добавляет существенных новых аспектов.
Ганс-Мартин Моснер
9
Это просто сокращение до абсурда. Если бы каждому классу буквально был разрешен только один метод, то буквально ни одна программа не могла бы сделать больше, чем одну вещь.
Даррел Хоффман
6
@DarrelHoffman Это не правда. Если бы каждый класс был функтором только с методом call (), то вы просто эмулировали простое процедурное программирование с объектно-ориентированным программированием. Вы все равно можете делать все, что могли бы сделать иначе, так как метод "call ()" класса может вызывать методы "call ()" многих других классов.
Вэл

Ответы:

29

Единственная ответственность не может быть чем-то, что может выполнять отдельная функция.

 class Location { 
     public int getX() { 
         return x;
     } 
     public int getY() { 
         return y; 
     } 
 }

Этот класс может нарушить принцип единственной ответственности. Не потому, что он имеет две функции, а если код getX()и getY()должен удовлетворять различные заинтересованные стороны, которые могут потребовать изменения. Если вице-президент г-н Х разослает памятку о том, что все числа должны быть выражены в виде чисел с плавающей запятой, а директор по бухгалтерскому учету г-жа Y настаивает на том, чтобы все числа, которые рассматривает ее отдел, оставались целыми числами независимо от того, что г-н Х считает хорошим, тогда этот класс лучше иметь единственное представление о том, перед кем он несет ответственность, потому что все может запутаться.

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

Кроме таких вещей. Нет, несколько методов в порядке. Просто дайте ему имя, которое прояснит, какие методы принадлежат классу, а какие нет.

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

«Модуль должен отвечать за одного и только одного актера»

Роберт С. Мартин - Чистая архитектура

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

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

Если вы применяете закон Керли к классу, все в классе должно быть об одной объединяющей идее. Эта идея может быть широкой. Идея может быть постоянством. Если там есть некоторые служебные функции регистрации, то они явно неуместны. Не имеет значения, является ли мистер X единственным, кто заботится об этом коде.

Классический принцип, применяемый здесь, называется разделением интересов . Если вы разделяете все свои проблемы, можно утверждать, что то, что осталось в каком-то одном месте, является одной проблемой. Это то, что мы называли этой идеей до того, как фильм 1991 года «Городские пижоны» познакомил нас с персонажем Керли.

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

Может быть, вы не заботитесь об этом. Хорошо. Мысль о том, что удержание «сделать что-то» решит все ваши проблемы с дизайном, демонстрирует недостаток фантазии о том, чем может быть «одна вещь». Другая причина ограничить сферу - организация. Вы можете вкладывать много «одного» в другое «одно», пока у вас не появится ящик для мусора, полный всего. Я говорил об этом раньше

Конечно, классическая причина ООП для ограничения области действия состоит в том, что у класса есть закрытые поля, и вместо того, чтобы использовать геттеры для совместного использования этих данных, мы помещаем каждый метод, которому нужны эти данные, в класс, где они могут использовать эти данные конфиденциально. Многие считают это слишком ограничительным для использования в качестве ограничителя области, потому что не каждый метод, который принадлежит друг другу, использует абсолютно одинаковые поля. Мне нравится следить за тем, чтобы любая идея, которая объединила данные, была той же идеей, что и методы.

Функциональный способ смотреть на это , что a.f(x)и a.g(x)просто е с (х) и г (х). Не две функции, а континуум пар функций, которые изменяются вместе. Даже не иметь в нем данные. Это может быть просто , как вы знаете , какие и реализации вы собираетесь использовать. Функции, которые меняются вместе, принадлежат друг другу. Это старый добрый полиморфизм.afg

SRP - только одна из многих причин ограничить область применения. Это хорошо. Но не единственный.

candied_orange
источник
25
Я думаю, что этот ответ сбивает с толку тех, кто пытается выяснить SRP. Битва между господином президентом и госпожой директором не решается техническими средствами, и использование ее для обоснования инженерного решения бессмысленно. Закон Конвея в действии.
whatsisname
8
@whatsisname Напротив. SRP был явно предназначен для применения к заинтересованным сторонам. Это не имеет ничего общего с техническим дизайном. Вы можете не согласиться с этим подходом, но именно так SRP был первоначально определен дядей Бобом, и ему приходилось повторять его снова и снова, потому что по какой-то причине люди, кажется, не в состоянии понять это простое понятие (ум, это действительно полезно, это совершенно ортогональный вопрос).
Луан
Закон Керли, описанный Тимом Оттингером, подчеркивает, что переменная должна постоянно означать одно. Для меня SRP немного сильнее, чем это; класс может концептуально представлять «одну вещь», но нарушать SRP, если два внешних драйвера изменения по-разному относятся к некоторому аспекту этой «одной вещи» или обеспокоены двумя разными аспектами. Проблема заключается в моделировании; вы решили смоделировать что-то как отдельный класс, но есть что-то в области, что делает этот выбор проблематичным (вещи начинают мешать вам по мере развития кодовой базы).
Филипп Милованович
2
@ FilipMilovanović Сходство, которое я вижу между законом Конвея и SRP в том смысле, в каком они были, дядя Боб объяснил SRP в своей книге «Чистая архитектура», исходит из предположения, что у организации есть чистая ациклическая организационная диаграмма. Это старая идея. Даже в Библии есть цитата: «Никто не может служить двум господам».
candied_orange
1
@TKK Я связываю это (не приравнивая) к закону Конвея, а не керли. Я опровергаю идею о том, что SRP является законом Керли, главным образом потому, что сам дядя Боб сказал об этом в своей книге «Чистая архитектура».
candied_orange
48

Ключевым моментом здесь является объем или, если вы предпочитаете, гранулярность . Часть функциональности, представленная классом, может быть дополнительно разделена на части функциональности, каждая из которых является методом.

Вот пример. Представьте, что вам нужно создать CSV из последовательности. Если вы хотите быть совместимым с RFC 4180, для реализации алгоритма и обработки всех крайних случаев потребуется довольно много времени.

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

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

Теперь все эти методы имеют одну общую цель: взять последовательность и создать CSV. Это единственная ответственность класса .


Пабло Н прокомментировал:

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

На самом деле. Пример CSV, который я привел, имеет в идеале один публичный метод, а все остальные методы являются частными. Лучшим примером будет очередь, реализованная Queueклассом. Этот класс будет содержать, в основном, два метода: push(также называемый enqueue) и pop(также вызываемый dequeue).

  • Ответственность Queue.pushзаключается в добавлении объекта в хвост очереди.

  • Ответственность Queue.popзаключается в том, чтобы удалить объект из головы очереди и обработать случай, когда очередь пуста.

  • Ответственность Queueкласса заключается в предоставлении логики очереди.

Арсений Мурзенко
источник
1
Хороший пример, но я чувствую, что он все еще не отвечает, почему SRP позволяет классу иметь более одного открытого метода.
Пабло H
1
@PabloH: честно. Я добавил еще один пример, в котором у класса есть два метода.
Арсений Мурзенко
30

Функция есть функция.

Ответственность есть ответственность.

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

Контейнерный класс (список, массив, словарь, карта и т. Д.) Отвечает за хранение объектов, что включает их хранение, возможность вставки, предоставления доступа, некоторый порядок упорядочения и т. Д.

Отдельная ответственность не означает, что кода / функциональности очень мало, это означает, что любая имеющаяся функциональность «принадлежит вместе» под одной и той же ответственностью.

Питер
источник
2
СОГЛАСУЕМОСТИ. @Aulis Ronkainen - связать два ответа. А в соответствии с вашими механическими аналогиями ответственность за техническое обслуживание транспортных средств лежит на гараже. разные механики в гараже несут ответственность за разные части автомобиля, но каждая из этих механик работает вместе в
единстве
2
@wolfsshield, согласился. Механик, который делает только одну вещь, бесполезен, но механик, который несет единственную ответственность, не является (по крайней мере, обязательно). Хотя реальные аналогии не всегда являются лучшими для описания абстрактных концепций ООП, важно различать эти различия. Я считаю, что непонимание различий - это то, что создает путаницу в первую очередь.
Аулис Ронкайнен
3
@AulisRonkainen Хотя это выглядит, пахнет и ощущается как аналогия, я намеревался использовать механику, чтобы подчеркнуть конкретное значение термина « Ответственность» в ПСП. Я полностью согласен с вашим ответом.
Петр
20

Отдельная ответственность не обязательно означает, что она делает только одно.

Возьмем, например, класс обслуживания пользователя:

class UserService {
    public User Get(int id) { /* ... */ }
    public User[] List() { /* ... */ }

    public bool Create(User u) { /* ... */ }
    public bool Exists(int id) { /* ... */ }
    public bool Update(User u) { /* ... */ }
}

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

SRP не следует путать с «принципом разделения интерфейса» (см. SOLID ). Принцип сегрегации интерфейса (ISP) говорит, что меньшие, легкие интерфейсы предпочтительнее, чем большие, более обобщенные интерфейсы. Go широко использует ISP в своей стандартной библиотеке:

// Interface to read bytes from a stream
type Reader interface {
    Read(p []byte) (n int, err error)
}

// Interface to write bytes to a stream
type Writer interface {
    Write(p []byte) (n int, err error)
}

// Interface to convert an object into JSON
type Marshaler interface {
    MarshalJSON() ([]byte, error)
}

SRP и ISP, безусловно, связаны между собой, но одно не подразумевает другого. Интернет-провайдер находится на уровне интерфейса, а SRP - на уровне класса. Если класс реализует несколько простых интерфейсов, он может перестать иметь только одну ответственность.

Спасибо Луаану за то, что он указал на разницу между ISP и SRP.

Джесси
источник
3
На самом деле, вы описываете принцип разделения интерфейса («Я» в SOLID). SRP это совсем другой зверь.
Луан
Кроме того, какое соглашение о кодировании вы используете здесь? Я бы ожидать , что объекты UserService и Userбыть UpperCamelCase, но методы Create , Existsи Updateя бы сделал lowerCamelCase.
KlaymenDK
1
@KlaymenDK Вы правы, заглавные буквы - это просто привычка использовать Go (прописные = экспортированные / публичные, строчные = частные)
Джесси
@Luaan Спасибо за указание на это, я уточню свой ответ
Джесси
1
@KlaymenDK Многие языки используют PascalCase для методов и классов. C # например.
Омегастик
15

В ресторане есть шеф-повар. Его единственная обязанность - готовить. Тем не менее, он может готовить стейки, картофель, брокколи и сотни других вещей. Вы бы наняли одного шеф-повара на блюдо в вашем меню? Или один шеф-повар для каждого компонента каждого блюда? Или один повар, который может выполнить свою единственную обязанность: готовить?

Если вы попросите этого повара также выполнить платежную ведомость, тогда вы нарушите SRP.

gnasher729
источник
4

Контрпример: хранение изменяемого состояния.

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

public class State {
    private int i;


    public State(int i) { this.i = i; }
}

Если вы ограничены только одним методом, вы можете использовать либо a setState(), либо a getState(), если вы не нарушите инкапсуляцию и не сделаете iобщедоступным.

  • Установщик бесполезен без получателя (вы никогда не сможете прочитать информацию)
  • Получатель бесполезен без установщика (вы никогда не можете изменить информацию).

Очевидно, что эта единственная ответственность требует наличия как минимум двух методов для этого класса. QED.

Александр
источник
4

Вы неверно истолковываете принцип единственной ответственности.

Единственная ответственность не равняется единственному методу. Они имеют в виду разные вещи. В разработке программного обеспечения мы говорим о сплоченности . Функции (методы), которые имеют высокую степень сцепления, «принадлежат» друг другу и могут рассматриваться как выполняющие одну ответственность.

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

Аулис Ронкайнен
источник
2

Часто полезно (на любом языке, но особенно на ОО-языках) смотреть на вещи и упорядочивать их с точки зрения данных, а не функций.

Таким образом, рассмотрим ответственность класса за поддержание целостности и предоставление помощи в правильном использовании данных, которыми он владеет. Очевидно, что это легче сделать, если весь код находится в одном классе, а не распределен по нескольким классам. Добавление двух пунктов выполняется более надежно, и код легче поддерживать с помощью Point add(Point p)метода в Pointклассе, чем в другом месте.

И, в частности, класс не должен предоставлять ничего, что могло бы привести к противоречивым или неправильным данным. Например, если a Pointдолжно лежать в плоскости (0,0) - (127,127), конструктор и любые методы, которые модифицируют или производят новое, Pointнесут ответственность за проверку заданных им значений и отклонение любых изменений, которые нарушают это. требование. (Часто что-то вроде a Pointбудет неизменным, и обеспечение того, что нет никаких способов изменить a Pointпосле его создания, также будет ответственностью класса)

Обратите внимание, что расслоение здесь вполне приемлемо. У вас может быть Pointкласс для работы с отдельными точками и Polygonкласс для работы с набором Points; у них все еще есть отдельные обязанности, потому что Polygonделегирует всю ответственность за то, чтобы иметь дело с чем-либо, имеющим исключительно отношение к классу Point(например, обеспечение точки имеет xи yзначение, и ценность) Point.

Курт Дж. Сэмпсон
источник