Як у класі може бути кілька методів, не порушуючи принципу єдиної відповідальності


64

Принцип єдиної відповідальності визначається у wikipedia як

Принцип єдиної відповідальності - це принцип комп’ютерного програмування, який стверджує, що кожен модуль, клас чи функція повинні нести відповідальність за окрему частину функціональних можливостей, що надаються програмним забезпеченням, і ця відповідальність повинна бути повністю капсульована класом

Якщо клас повинен мати лише одну відповідальність, то як він може мати більше 1 методу? Чи не кожен метод по-різному відповідав би, це означало б, що клас матиме більше 1 відповідальності.

Кожен приклад, який я бачив, демонструючи принцип єдиної відповідальності, використовує клас прикладу, який має лише один метод. Це може допомогти побачити приклад або пояснити клас декількома методами, які все ще можуть вважатись однією відповідальністю.


11
Чому потік? Здається, ідеальне питання для SE.SE; людина досліджувала цю тему і доклала зусиль, роблячи питання дуже зрозумілим. Замість цього він заслуговує на оплату коштів.
Арсеній Муренко

19
Очевидно, що це було пов'язано з тим, що це питання, на яке вже задавались і відповіли вже декілька разів, наприклад див. Softwareengineering.stackexchange.com/questions/345018/… . На мою думку, це не додає істотних нових аспектів.
Ганс-Мартін Моснер


9
Це просто зменшення ad absurdum. Якби в кожному класі буквально був дозволений лише один метод, тоді буквально немає можливості жодній програмі зробити щось більше, ніж одну справу.
Даррель Гофман

6
@DarrelHoffman Це неправда. Якщо кожен клас був функтором, що використовує лише метод "call ()", то ви в основному просто імітували звичайне процедурне програмування з об'єктно-орієнтованим програмуванням. Ви все ще можете робити все, що могли зробити в іншому випадку, оскільки метод class'es "call ()" може викликати багато інших класів "" call () "методів.
Ваелюс

Відповіді:


29

Одинична відповідальність може бути не тим, що може виконувати одна функція.

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

Цей клас може порушити принцип єдиної відповідальності. Не тому, що він має дві функції, але якщо код getX()і getY()повинен задовольняти різні зацікавлені сторони, які можуть вимагати зміни. Якщо віце-президент пан Х надішле записку про те, що всі номери повинні бути виражені цифрами з плаваючою комою, а директор бухгалтерії пані Y наполягає на тому, що всі числа, які переглядає її відділ, залишатимуться цілими числами незалежно від того, що містер X вважає добре, тоді цей клас мав би краще мати єдина ідея того, кому це відповідально, тому що речі можуть заплутатися.

Якщо SRP дотримувались, було б зрозуміло, чи би клас класу вносив внесок у те, до чого містер Х та його група схильні. Поясніть, за що відповідає клас, і ви знаєте, яка директива впливає на цей клас. Якщо вони обидва впливають на цей клас, то він був погано розроблений, щоб мінімізувати вплив змін. "Клас повинен мати лише одну причину для зміни" не означає, що весь клас може робити лише одну дрібницю. Це означає, що я не маю змоги подивитися на клас і сказати, що і містер Х, і місіс Ю мають інтерес до цього класу.

Крім подібних речей. Ні, кілька методів добре. Просто дайте йому назву, яка дає зрозуміти, які методи належать до класу, а які - ні.

SRP дядька Боба більше стосується закону Конвея, ніж закону Керлі . Дядько Боб виступає за застосування закону Керлі (виконайте одне) до функцій, а не до класів. SRP застерігає від змішування причин, щоб разом змінюватися. Закон Конвея говорить, що система буде стежити за потоком інформації організації. Це призводить до слідування SRP, оскільки вас не хвилює те, про що ви ніколи не чуєте.

"Модуль повинен відповідати одному та лише одному актору"

Роберт С Мартін - чиста архітектура

Люди продовжують хотіти, щоб SRP був про всі причини, щоб обмежити сферу застосування. Є більше причин для обмеження обсягу, ніж SRP. Я ще більше обмежую сферу, наполягаючи на тому, що клас буде абстракцією, яка може прийняти ім’я, яке гарантує, що зазирнути всередину вас не здивує .

Ви можете застосувати Закон Керлі до класів. Ти поза тим, про що говорить дядько Боб, але ти можеш це зробити. Куди ви підете не так - це коли ви починаєте думати, що це означає одну функцію. Це як думати, що в сім'ї має бути лише одна дитина. Маючи більше ніж одну дитину, це не перешкоджає бути сім’єю.

Якщо застосувати закон Керлі до класу, все в класі має бути приблизно однією об'єднавчою ідеєю. Ця ідея може бути широкою. Ідея може бути наполегливістю. Якщо деякі функції утиліти журналу є там, вони явно не в силі. Не має значення, містер Х - єдиний, хто піклується про цей код.

Класичний принцип, який слід застосувати тут, називається відокремленням проблем . Якщо ви розділите всі свої занепокоєння, можна стверджувати, що те, що залишилося в будь-якому одному місці, - це одна турбота. Саме так ми назвали цю ідею ще до того, як фільм 1991 року «Сіті Слайкери» познайомив нас із персонажем Кучерявого.

Це добре. Просто те, що дядько Боб називає відповідальністю, не викликає занепокоєння. Відповідальність за нього - це не те, на чому ти зосереджений. Це щось, що може змусити вас змінитися. Ви можете зосередитись на одній проблемі і все ще створити код, який відповідає різним групам людей з різними програмами.

Можливо, тобі це не байдуже. Чудово. Думаючи, що затримання "робити одну річ" вирішить усі ваші дизайнерські неприємності, демонструє відсутність уяви про те, що "одна річ" може виявитись. Ще одна причина обмеження сфери - організація. Ви можете вкласти багато "однієї речі" всередину іншої "однієї речі", поки у вас не буде повноцінного шухляди. Я говорив про це раніше

Звичайно, класична причина OOP обмежувати область полягає в тому, що клас має в ньому приватні поля, а замість цього використовувати getters для обміну цими даними навколо, ми ставимо кожен метод, який потребує цих даних у класі, де вони можуть використовувати дані приватно. Багато хто вважає це занадто обмежуючим, щоб використовувати його як обмежувач області, оскільки не кожен метод, що належить разом, використовує абсолютно однакові поля. Я хотів би переконатися, що будь-яка ідея, яка об'єднала дані, буде тією самою ідеєю, яка об'єднала методи.

Функціональний спосіб дивитися на це , що a.f(x)і a.g(x)просто е з (х) і г (х). Не дві функції, а континуум пар функцій, які різняться між собою. Навіть не мати в ньому дані. Це може бути просто те, як ви знаєте, яку і реалізацію ви збираєтеся використовувати. Функції, що змінюються разом, належать разом. Це гарний старий поліморфізм.afg

SRP - лише одна з багатьох причин обмеження сфери застосування. Це добре. Але не єдиний.


25
Я думаю, що ця відповідь бентежить того, хто намагається з'ясувати СРП. Бій між паном президентом та пані директором не вирішується технічними засобами, і використовувати його для обгрунтування інженерного рішення є безглуздим. Закон Конвея в дії.
whatsisname

8
@whatsisname Навпаки. СРП явно мав на меті застосувати до зацікавлених сторін. Це не має нічого спільного з технічним дизайном. Ви можете не погодитися з таким підходом, але саме так ДРП був спочатку визначений дядьком Боб, і йому довелося повторювати це знову і знову, оскільки чомусь люди, здається, не можуть зрозуміти це просте поняття (розум, чи це насправді корисно - це абсолютно ортогональне питання).
Луань

Закон Керлі, як описав Тім Оттінгер, підкреслює, що змінна повинна послідовно означати одне. Для мене SRP трохи сильніший за це; клас може концептуально представляти "одне", але порушувати SRP, якщо два зовнішніх драйвера змін трактують якийсь аспект цієї "однієї речі" по-різному, або переймаються двома різними аспектами. Проблема полягає в моделюванні; Ви вирішили моделювати щось як єдиний клас, але є щось про домен, який робить цей вибір проблематичним (все починає змінюватися у міру розвитку бази даних коду).
Філіп Мілованович

2
@ FilipMilovanović Подібність, яку я бачу між Законом Конвея та SRP, так, як дядько Боб пояснив SRP у своїй книзі «Чиста архітектура», випливає з припущення, що організація має чисту ациклічну організаційну діаграму. Це стара ідея. Навіть у Біблії тут є цитата: «Жодна людина не може служити двом панам».
candied_orange

1
@TKK im пов'язує це (не прирівнюючи його) до закону Конвейса, а не до закону Керлі. Я спростовую думку про те, що 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 дозволяє класу мати більше одного публічного методу.
Пабло Н

1
@PabloH: справедливо. Я додав ще один приклад, коли клас має два методи.
Арсеній Муренко

30

Функція - це функція.

А відповідальність - це відповідальність.

Механік несе відповідальність за виправлення автомобілів, що передбачає діагностику, деякі прості завдання з технічного обслуговування, деякі фактичні ремонтні роботи, деякі делегування завдань іншим тощо.

Клас контейнерів (список, масив, словник, карта тощо) несе відповідальність за зберігання об'єктів, що передбачає їх зберігання, дозволяючи вставляти, надавати доступ, якесь замовлення тощо.

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


2
Збіг. @Aulis Ronkainen - зв'язати дві відповіді. А за вкладені обов'язки за гарантією механіки відповідальність за обслуговування транспортних засобів має гараж. Різні механіки в гаражі несуть відповідальність за різні частини автомобіля, але кожен з цих механіків працює разом у згуртованості
wolfsshield

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) { /* ... */ }
}

У цьому класі є кілька методів, але відповідальність його зрозуміла. Він забезпечує доступ до записів користувачів у сховищі даних. Єдині її залежності - це модель користувача та сховище даних. Це слабко пов'язане і дуже згуртоване, що насправді саме те, що 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, безумовно, пов'язані, але одне не означає іншого. ISP знаходиться на рівні інтерфейсу, а SRP - на рівні класу. Якщо клас реалізує кілька простих інтерфейсів, він може не мати лише однієї відповідальності.

Дякуємо Луаану за вказівку на різницю між ISP та SRP.


3
Насправді ви описуєте принцип сегрегації інтерфейсу ("Я" в SOLID). СРП зовсім інший звір.
Луань

Як осторонь, яку умову кодування ви використовуєте тут? Я б очікувати , що об'єкти UserService і Userбути UpperCamelCase, але методи Create , Existsі Updateя б зробив lowerCamelCase.
KlaymenDK

1
@KlaymenDK Ви праві, великі регістри - це лише звичка використовувати Go (верхній регістр = експортований / загальнодоступний, нижній регістр = приватний)
Джессі

@Luaan Дякую, що вказав на це, я поясню свою відповідь
Джессі

1
@KlaymenDK Багато мов використовують PascalCase як для методів, так і для класів. C # наприклад.
Омегастик

15

В ресторані є шеф-кухар. Його єдина відповідальність - готувати. І все-таки він може готувати стейки, картоплю, брокколі та сотні інших речей. Ви б найняли одного шеф-кухаря на страву у своєму меню? Або один шеф-кухар для кожного компонента кожної страви? Або один шеф-кухар, який може відповідати його єдиній відповідальності: Готувати?

Якщо ви попросите цього шеф-кухаря також виконати нарахування заробітної плати, тоді ви порушите SRP.


4

Контрприклад: зберігання змінного стану.

Припустимо, у вас був найпростіший клас будь-коли, єдиним завданням якого є зберігання int.

public class State {
    private int i;


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

Якщо ви обмежилися лише одним методом, ви можете мати або a setState(), або getState(), якщо ви не порушите інкапсуляцію та iоприлюднити.

  • Сеттер марний без геттера (ви ніколи не могли прочитати інформацію)
  • Геттер марний без сетера (ви ніколи не можете мутувати інформацію).

Очевидно, що ця єдина відповідальність потребує принаймні 2 методи цього класу. QED.


4

Ви неправильно трактуєте принцип єдиної відповідальності.

Однозначна відповідальність не дорівнює одному методу. Вони означають різні речі. У розробці програмного забезпечення ми говоримо про згуртованість . Функції (методи), які мають високу згуртованість, "належать" разом і можуть вважатися такими, що виконують єдину відповідальність.

Розробник повинен розробити систему так, щоб виконувався принцип єдиної відповідальності. Можна сприймати це як техніку абстрагування, тому іноді це питання думки. Реалізація принципу єдиної відповідальності робить код в основному простішим для тестування та простішим розумінням його архітектури та дизайну.


2

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

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

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

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.