Уточнити єдиний принцип відповідальності


64

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

Як би ви описали, що означає "одна річ"? Які конкретні ознаки того, що клас справді робить більше, ніж «одне»?


6
+1 для верхнього "коду равіолі". На початку своєї кар’єри я був одним із тих людей, які зайняли це занадто далеко. Не тільки з класами, але і з модуляризацією методів. Мій код був переповнений безліччю маленьких методів, які зробили щось просте, просто заради розбиття проблеми на невеликі шматки, які могли вміститися на екрані без прокрутки. Очевидно, це часто йшло дуже далеко.
Столи Бобі

Відповіді:


50

Мені дуже подобається, як Роберт К. Мартін (дядько Боб) переосмислює єдиний принцип відповідальності (пов'язаний з PDF) :

Ніколи не повинно бути більше ніж одна причина для зміни класу

Це тонко відрізняється від традиційного визначення "слід робити тільки одне", і мені це подобається, тому що воно змушує змінити те, як ти думаєш про свій клас. Замість того, щоб думати про те, "це робиться одне?", Ви замість цього думаєте про те, що може змінитися і як ці зміни впливають на ваш клас. Наприклад, якщо зміна бази даних потребує змін вашого класу? А як бути, якщо пристрій виводу зміниться (наприклад, екран, мобільний пристрій чи принтер)? Якщо ваш клас потребує змін через зміни в багатьох інших напрямках, то це знак того, що ваш клас має занадто багато обов'язків.

У пов'язаній статті дядько Боб робить висновок:

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


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

Це насправді відмінний спосіб поставити це. Мені це подобається. Як зауваження, я, як правило, думаю, що СРП застосовується набагато сильніше до методів. Іноді клас просто повинен робити дві речі (можливо, його клас мостить два домени), але метод майже ніколи не повинен робити більше, ніж те, що можна коротко описати за його підписом типу.
CodexArcanum

1
Щойно показав це моєму Град - чудове читання і прокляте гарне нагадування про себе.
Martijn Verburg

1
Гаразд, це має сенс, коли ви поєднуєте його з думкою про те, що неперевірений код повинен планувати лише зміни, що можливо, в найближчому майбутньому, а не для всіх можливих змін. Тоді я повторю це, трохи, оскільки "має бути лише одна причина зміни класу, яка, можливо, відбудеться в осяжному майбутньому". Це заохочує віддати перевагу простоті в частинах конструкції, які навряд чи зміняться та роз’єднаються у частинах, які можуть змінитися.
dimimcha

18

Я постійно запитую себе, яку проблему намагається вирішити SRP? Коли SRP мені допомагає? Ось що я придумав:

Ви повинні відповідальність / функціональність рефактора поза класом, коли:

1) Ви скопіювали функціональність (DRY)

2) Ви виявите, що ваш код потребує іншого рівня абстракції, щоб допомогти вам зрозуміти це (KISS)

3) Ви виявляєте, що спеціалісти вашого домену розуміють, що фрагменти функціональності є окремими компонентами (всюдисуща мова)

Ви не повинні нести відповідальність за рефактор поза класом, коли:

1) Існує не дублюється функціональність.

2) Функціонал не має сенсу поза контекстом вашого класу. По-іншому, ваш клас забезпечує контекст, в якому простіше зрозуміти функціональність.

3) Ваші експерти з домену не мають такої відповідальності.

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

Коли сумніваєтесь, залишайте це! Ви завжди можете пізніше змінити рефактор, коли це буде чіткий випадок.

Що ти думаєш?


Хоча ці вказівки можуть чи не можуть бути корисними, але насправді це не має нічого спільного з SRP, визначеним у SOLID, чи не так?
сара

Спасибі. Я не можу повірити деяким божевіллям, пов'язаним з так званими принципами SOLID, де вони роблять дуже простий код у сто разів складнішим без поважних причин . Наведені вами описують реальні причини впровадження SRP. Я думаю, що принципи, які ви подали вище, повинні стати їх власною абревіатурою, і викинути "ТВЕРДО", це приносить більше шкоди, ніж користі. "Астронавти архітектури" справді, як ви вказали в нитці, яка веде мене сюди.
Микола Петерсен

4

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


4

Відповідь лежить у визначенні

Те, що ви визначаєте відповідальність, в кінцевому рахунку дає вам межу.

Приклад:

У мене є компонент, відповідальний за відображення рахунків-фактур -> в цьому випадку, якщо я почав додавати щось більше, то я порушую принцип.

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

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


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

1
По суті, ОП запитує "Як ви визначаєте відповідальність?" тому, коли ви кажете, що відповідальність є такою, яку ви її визначаєте, здається, це просто повторення питання.
Деспертар

2

Я завжди переглядаю це на двох рівнях:

  • Я переконуюсь, що мої методи роблять лише одне і роблять це добре
  • Я розглядаю клас як логічне (OO) групування тих методів, що добре представляє одне

Так щось на зразок об’єкта домену під назвою Dog:

Dogце мій клас, але Собаки вміють робити багато речей! У мене можуть бути такі методи, як walk(), run()і bite(DotNetDeveloper spawnOfBill)(вибачте, не втримався; p).

Якщо це Dogстає непростим, тоді я б подумав про те, як групи цих методів можна моделювати разом в іншому класі, наприклад, Movementкласі, який міг би містити мої walk()та run()методи.

Не існує жодного жорсткого і швидкого правила, ваш дизайн OO буде розвиватися з часом. Я намагаюся знайти чіткий інтерфейс / публічний API, а також прості методи, які добре роблять одне і одне.


Bite дійсно має взяти примірник Object, а DotNetDeveloper має бути підкласом Person (як правило, все одно!)
Алан Пірс

@Alan - Там - виправили це для вас :-)
Martijn Verburg

1

Я дивлюся на це більше за класом, який повинен представляти лише одне. Присвоїти @ Наприклад Karianna, я є свій Dogклас, який має методи walk(), run()і bark(). Я не збираюся додавати методи meaow(), squeak(), slither()або fly()тому , що це не ті речі , які роблять собаки. Це речі, які роблять інші тварини, і ті інші тварини мали б власні класи, щоб їх представляти.

(До речі, якщо ваша собака дійсно літати, то ви , ймовірно , слід припинити кидати його з вікна).


+1 за "якщо ваша собака летить, то вам, мабуть, слід перестати викидати її у вікно". :)
Столи Бобі

Крім питання про те, що повинен представляти клас , що представляє екземпляр ? Якщо хтось розглядає SeesFoodяк характеристику DogEyes, Barkяк щось, що робиться а DogVoice, і Eatяк щось, що робиться а DogMouth, то така логіка if (dog.SeesFood) dog.Eat(); else dog.Bark();стане такою if (eyes.SeesFood) mouth.Eat(); else voice.Bark();, що втрачає будь-яке відчуття ідентичності, що очі, рот і голос пов'язані з єдиним правом.
supercat

@supercat це справедливий момент, хоча контекст важливий. Якщо код, який ви згадуєте, знаходиться в Dogкласі, то, ймовірно, Dogпов'язаний. Якщо ні, то, ймовірно, ви б закінчилися чимось на кшталт, myDog.Eyes.SeesFoodа не просто eyes.SeesFood. Інша можливість полягає в тому, що Dogвідкривається ISeeінтерфейс, який вимагає Dog.Eyesвластивості та SeesFoodметоду.
JohnL

@JohnL: Якщо фактична механіка бачення керується собачими очима, по суті так само, як котячі або зебри, то, можливо, має сенс керувати механікою Eyeклас, але собака повинна "бачити" використовуючи очі, а не просто мати очі, які можуть бачити. Собака - це не око, але це не просто око. Це "річ, яку можна [принаймні спробувати] побачити", і її слід описати через інтерфейс як таку. Навіть сліпу собаку можна запитати, чи бачить вона їжу; це не буде дуже корисно, оскільки собака завжди скаже «ні», але в питанні немає шкоди.
supercat

Тоді ви використовуєте інтерфейс ISee, як я описав у своєму коментарі.
JohnL

1

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

Я використовую назви класів як тест для цього. Якщо я не можу дати класу досить коротке описове ім’я, або якщо в цьому імені є слово типу "І", я, ймовірно, порушую Принцип єдиної відповідальності.

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


0

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

Кожен клас повинен бути поновлений назвою ролей. Насправді роль - це (набір) дієслова, пов'язаного з контекстом.

Наприклад :

Файл забезпечує доступ до файлу. FileManager керує файловими об'єктами.

Дані про вміщення ресурсу для одного ресурсу з файлу. ResourceManager утримує та надає всі ресурси.

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

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

Я часто будую класи менеджерів, у яких є кілька різних інших класів. Як Фабрика, Реєстр тощо. Дивіться клас менеджера, як якийсь начальник групи, керівник оркестру, який спрямовує інших людей працювати разом для досягнення ідеї високого рівня. Він має одну роль, але передбачає роботу з іншими унікальними ролями всередині. Ви також можете бачити це як організація компанії: генеральний директор не є продуктивним на чистому рівні продуктивності, але якщо його там немає, то нічого спільно працювати не може. Ось його роль.

Створюючи дизайн, визначте унікальні ролі. І для кожної ролі ще раз перевірте, чи не можна її вирішити в кількох інших ролях. Таким чином, якщо вам потрібно просто змінити спосіб, як ваш менеджер будує об'єкти, просто змініть Фабрику та йдіть з думкою про мир.


-1

SRP - це не лише поділ класів, а й делегування функціональних можливостей.

У наведеному вище прикладі собаки не використовуйте SRP як обґрунтування, щоб мати 3 окремі класи, такі як DogBarker, DogWalker тощо (низька згуртованість). Натомість подивіться на реалізацію методів класу та вирішіть, чи "вони занадто багато знають". Ви все ще можете мати dog.walk (), але, ймовірно, метод walk () повинен делегувати іншому класу подробиці того, як здійснюється ходьба.

Насправді ми дозволяємо класу Собаки мати одну причину зміни: тому що Собаки змінюються. Звичайно, поєднуючи це з іншими принципами SOLID, ви б розширили Собаку на нову функціональність, а не на зміну Собаки (відкрита / закрита). І ви б ввели свої залежності, такі як IMove та IEat. І звичайно, ви б зробили ці окремі інтерфейси (інтерфейсна сегрегація). Собака змінилася б лише в тому випадку, якщо ми знайшли помилку або якщо Собаки кардинально змінилися (Ліськов Суб, не поширюй і не видаляй поведінку).

Чистий ефект SOLID полягає в тому, що нам доводиться писати новий код частіше, ніж нам доводиться змінювати існуючий код, і це великий виграш.


-1

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

І як хтось сказав: "Ходити по воді та розробляти програмне забезпечення із заданих специфікацій легко, за умови, що обидва заморожені".

Отже, якщо ми визначаємо відповідальність більш конкретно, то нам її не потрібно міняти.

Іноді обов'язки будуть очевидно очевидні, але іноді це може бути тонко, і нам потрібно зважено вирішувати.

Припустимо, ми додаємо ще одну відповідальність до класу Dog під назвою catchTentist (). Тепер це може призвести до додаткової різної відповідальності. Завтра, якщо спосіб, коли Собака ловить злодія, повинен змінити відділ поліції, тоді клас Собаки доведеться змінити. У цьому випадку було б краще створити інший підклас і назвати його ThiefCathcerDog. Але по-іншому, якщо ми впевнені, що це не зміниться за будь-яких обставин, або спосіб, яким було реалізовано catchTentist, залежить від якогось зовнішнього параметра, тоді ця відповідальність буде цілком нормальною. Якщо відповідальність не є надзвичайно дивною, тоді ми повинні вирішити її обґрунтовано, виходячи із випадку використання.


-1

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


1
Одна з причин зміни не має нічого спільного з кількістю випадків використання, дійовими особами або наскільки ймовірними є зміни. Вам не слід робити список можливих змін. Правильно, що одна зміна повинна впливати лише на один клас. Вміти розширити клас, щоб прийняти цю зміну, це добре, але це відкритий закритий принцип, а не SRP.
candied_orange

Чому ми не повинні складати список найбільш ймовірних змін? Спекулятивний дизайн?
kiwicomb123

оскільки це не допоможе, не буде повноцінним, і є більш ефективні способи боротьби зі змінами, ніж намагатися передбачити. Просто очікуйте цього. Ізолюючі рішення і вплив будуть мінімальними.
candied_orange

Гаразд, я розумію, це порушує принцип "не потрібно гоні".
kiwicomb123

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