Принцип єдиної відповідальності - як я можу уникнути фрагментації коду?


56

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

У нас є ситуація, коли він застосував SRP до того, що вже було досить складною базою коду, яка тепер стала дуже сильно фрагментованою і важкою для розуміння та налагодження.

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

У нас є кілька конструкторів класу, які приймають 20 параметрів інтерфейсу, тому наша реєстрація та дозвіл IoC стає монстром сама по собі.

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

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

Будь-які пропозиції ?


18
Це лише моя думка, але я думаю, що є ще одне правило, яке дуже легко забувається під купою різних абревіатур - "Принцип здорового почуття". Коли «рішення» створює більше проблем, які він справді вирішує, то щось не так. Я вважаю, що якщо проблема є складною, але вона укладена в клас, який піклується про її тонкощі і все ще відносно легко налагоджувати - я залишаю її в спокої. Як правило, ваша ідея "обгортки" здається мені звуковою, але відповідь я залишу комусь більш обізнаному.
Patryk Ćwiek

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

62
Клас з 20 параметрами конструктора для мене не дуже звучить!
MattDavey

1
Ви пишете "... реєстрація та дозвіл IoC ..."; це звучить так, ніби ви (або керівник вашої команди) думаєте, що "IoC" та "ін'єкція залежності" (DI) - це одне і те ж, що не відповідає дійсності. DI - це засіб для досягнення IoC, але, безумовно, не єдиний. Вам слід уважно проаналізувати, чому ви хочете робити IoC; якщо це тому, що ви хочете написати тестові одиниці, ви також можете спробувати скористатися шаблоном локатора обслуговування або просто класами інтерфейсу ( ISomething). ІМХО, з цими підходами обробляти набагато простіше, ніж введення залежності, і це призводить до отримання більш читабельного коду.

2
будь-яка відповідь, дана тут, була б у вакуумі; нам доведеться побачити код, щоб дати конкретну відповідь. 20 параметрів у конструкторі? ну, можливо, вам не вистачає об'єкта ... або вони можуть бути дійсними; або вони можуть належати у конфігураційному файлі, або вони можуть належати до класу DI, або ... Симптоми, безумовно, звучать підозріло, але, як і більшість речей у CS, "це залежить" ...
Стівен А. Лоу,

Відповіді:


84

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

Однак, SRP, як і більшість принципів там, може бути надмірно застосований. Якщо ви створюєте новий клас для збільшення цілих чисел, то так, це може бути однією відповідальністю, але давай. Це смішно. Ми зазвичай забуваємо, що такі речі, як принципи SOLID, існують за призначенням. СОЛІД - це засіб для досягнення мети, а не самоціль. Кінець - ремонтопридатність . Якщо ви збираєтеся отримати цю деталізацію з Принципом єдиної відповідальності, це показник того, що завзяття щодо SOLID засліпило команду до мети SOLID.

Отже, я здогадуюсь, що я говорю це ... ЗРП - це не ваша проблема. Це або нерозуміння СРП, або неймовірно детальне його застосування. Постарайтеся, щоб ваша команда зберігала головне головне. І головне - ремонтопридатність.

EDIT

Запропонуйте людям проектувати модулі таким чином, щоб заохочувати простоту використання. Подумайте про кожен клас як про міні-API. Спочатку подумайте: «Як би я хотів використовувати цей клас», а потім реалізуйте його. Не думайте просто "Що потрібно робити цьому класі". SRP має велику тенденцію ускладнювати використання класів, якщо ви не дуже замислюєтесь про зручність використання.

EDIT 2

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

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

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


2
Альтернативний спосіб змусити людей задуматися над розробкою класів: дозвольте їм написати CRC-картки (назва класу, відповідальність, колаборатори) . Якщо у класі занадто багато співробітників або обов'язків, він, швидше за все, недостатньо SRP. Іншими словами, весь текст повинен вміщуватися в індексній картці, інакше він робить занадто багато.
Спіке

18
Я знаю, що таке вогнемет, але як чорт ви ловите рибу?
Р. Мартіньо Фернандес

13
+1 СОЛІД - це засіб для досягнення мети, а не самоціль.
B Сім

1
+1: Я раніше заперечував, що такі речі, як "Закон Деметера", називаються неправильно, це має бути "Довідкова лінія Деметра". Ці речі повинні працювати для вас, ви не повинні працювати на них.
Бінарний страшник

2
@EmmadKareem: Це правда, що об'єкти DAO мають декілька властивостей. Але знову ж таки, є кілька речей, за допомогою яких можна згрупувати щось настільки просто, як Customerклас, і мати більш доцільний код. Дивіться приклади тут: codemonkeyism.com/…
Spoike

32

Я думаю, що саме в «Рефакторингу» Мартіна Фоулера я одного разу прочитав контр-правило SRP, визначаючи, куди це надто далеко. Є друге питання, таке важливе, як "чи має кожен клас лише одну причину для зміни?" і це "чи впливає кожна зміна лише на один клас?"

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

Наприклад, якщо додавання одного поля до таблиці означає, що вам доведеться змінити DTO і клас валідатора, клас стійкості та об'єкт моделі перегляду тощо, тоді ви створили проблему. Можливо, вам слід переосмислити, як ви реалізували SRP.

Можливо, ви сказали, що додавання поля є причиною зміни об’єкта Клієнта, але зміна рівня стійкості (скажімо, з XML-файлу в базу даних) - ще одна причина зміни об’єкта Замовника. Тож ви вирішили створити і об’єкт CustomerPersistence. Але якщо ви зробите це так, що для додавання поля ПОТРІБНО потрібно змінити об’єкт CustomerPersisitence, то в чому полягав сенс? У вас все-таки є об’єкт, який має дві причини зміни - він просто більше не є Замовником.

Однак якщо ви введете ORM, цілком можливо, що ви можете змусити класи працювати таким чином, що якщо ви додасте поле до DTO, воно автоматично змінить SQL, який використовується для читання цих даних. Тоді у вас є вагомі підстави розділити дві проблеми.

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


+1 для "чи кожна зміна впливає лише на один клас?"
dj18

Пов'язане питання, про яке я не бачив обговорюватись, полягає в тому, що якщо завдання, пов'язані з одним логічним об'єктом, будуть фрагментовані між різними класами, можливо, в коді може знадобитися посилання на кілька різних об'єктів, які прив'язані до однієї сутності. Розглянемо, наприклад, піч з функціями "SetHeaterOutput" і "MeasureTemperature". Якщо піч була представлена ​​незалежними об'єктами HeaterControl і TemperatureSensor, то ніщо не завадило б об'єкту TemperatureFeedbackSystem мати посилання на нагрівач однієї печі та інший датчик температури печі.
supercat

1
Якщо замість цього ці функції були об'єднані в інтерфейс IKiln, який був реалізований об'єктом Kiln, то для системи TemperatureFeedback потрібно було б містити лише одну посилання на IKiln. Якщо потрібно було використовувати піч з незалежним датчиком температури післяпродажної торгівлі, можна було б використовувати об'єкт CompositeKiln, конструктор якого прийняв IHeaterControl та ITemperatureSensor і використовував їх для реалізації IKiln, але такий навмисний пухкий склад буде легко впізнати в коді.
supercat

23

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

public class MyAwesomeClass {
    public class MyAwesomeClass(IDependency1 _d1, IDependency2 _d2, ... , IDependency20 _d20) {
      // Assign it all
    }
}

... тоді це стало занадто складним, і ви насправді не стежите за SRP , чи не так? Я ставлюся під заклад, якщо ви записували те, що MyAwesomeClassробиться на картці CRC, це не вміщується на індексній картці, або ви повинні писати справді крихітні нерозбірливі літери.

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

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

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

  1. Визначте всі дії (чи обов'язки), які робить ваш клас із залежностями.
  2. Групуйте дії відповідно до тісно пов'язаних залежностей.
  3. Повторний випуск! Тобто рефактор кожного з виявлених дій або до нових, або (що ще важливіше) інших класів.

Абстрактний приклад відповідальності рефакторингу

Нехай Cбуде клас , який має кілька залежностей D1, D2, D3, D4що вам потрібно реорганізувати , щоб використовувати менше. Коли ми визначаємо, якими методами Cвикликає залежність, ми можемо скласти простий її список:

  • D1- performA(D2),performB()
  • D2 - performD(D1)
  • D3 - performE()
  • D4 - performF(D3)

Дивлячись на список, ми можемо побачити це D1і D2пов'язані між собою, оскільки клас хоч якось потребує їх разом. Ми також можемо побачити, що D4потрібно D3. Отже, у нас є дві групи:

  • Group 1- D1<->D2
  • Group 2- D4->D3

Групування - це показник того, що зараз клас має два обов'язки.

  1. Group 1- Один для обробки виклику двох об'єктів, які потребують один одного. Можливо, ви можете дозволити своєму класу Cусунути необхідність поводження з обома залежностями та залишити один із них обробляти ці виклики. У цій групуванні очевидно, що це D1може мати посилання на D2.
  2. Group 2- Інша відповідальність потребує одного об'єкта для виклику іншого. Ви не можете D4впоратися D3замість вашого класу? Тоді ми, мабуть, можемо виключити D3з класу C, дозволивши D4робити дзвінки замість цього.

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


Редагувати:

Серед коментарів @Emmad Karem говорить:

"Якщо ваш клас має 20 параметрів у конструкторі, це не здається, що ваша команда зовсім знає, що таке SRP. Якщо у вас клас, який робить лише одне, то як він має 20 залежностей?" - Я думаю, що якщо ви мати клас клієнта, не дивно мати 20 параметрів у конструкторі.

Це правда, що для об'єктів DAO, як правило, є багато параметрів, які ви повинні встановити у своєму конструкторі, а параметри, як правило, є простими типами, такими як рядок. Однак у прикладі Customerкласу ви все ще можете згрупувати його властивості всередині інших класів, щоб зробити речі простішими. Такі , як має Addressклас з вулиць і Zipcodeклас , який містить поштовий індекс і буде обробляти бізнес - логіку , такі як перевірка достовірності даних , а також:

public class Address {
    private String street1;
    //...

    private Zipcode zipcode;

    // easy to extend
    public bool isValid() {
        return zipcode.isValid();
    }
}

public class Zipcode {
    private string zipcode;
    public bool isValid() {
        // return regex match that zipcode contains numbers
    }
}

Ця річ обговорюється далі у публікації блогу "Ніколи, ніколи, ніколи не використовуй String на Java (або принаймні часто)" . В якості альтернативи використання конструкторів або статичних методів для полегшення створення під об’єктів ви можете використовувати шаблон побудови рідини .


+1: Чудова відповідь! Групування є IMO дуже потужним механізмом, оскільки ви можете застосовувати групування рекурсивно. Якщо говорити дуже грубо, за допомогою n абстракційних шарів ви можете організувати 2 ^ n предметів.
Джорджо

+1: Ваші перші кілька пунктів підсумовують саме те, з чим стикається моя команда. "Бізнес-об'єкти", які насправді є об'єктами обслуговування, і код установки тестового блоку, який потрібно записувати. Я знав, що у нас виникла проблема, коли наші дзвінки рівня обслуговування міститимуть один рядок коду; виклик методу бізнес-рівня.
Людина

3

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

Під час роботи з Legacy системами "ентузіазм" виправити все, щоб покращити його, як правило, досить високий у лідерах команди, особливо тих, які є новими в цій ролі. SOLID, просто не має SRP - Це лише S. Переконайтесь, що якщо ви слідуєте за SOLID, ви також не забудете OLID.

Я працюю над системою Legacy зараз, і ми почали йти схожим шляхом на початку. Для нас працювало колективне рішення колективу зробити найкраще з обох світів - SOLID та KISS (Keep It Simple Stupid). Ми спільно обговорили основні зміни в структурі коду та застосували здоровий глузд у застосуванні різних принципів розробки. Вони чудові як настанови, а не "Закони розвитку с / ж". Команда полягає не лише в команді Lead - її про всіх розробників у команді. Те, що завжди працювало для мене, - це змусити всіх в кімнаті та придумати загальний набір рекомендацій, які погодиться дотримуватися вся ваша команда.

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


3

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

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

Крім SRP, існує багато інших стилів розробки. У вашому випадку звуків, схожих на YAGNI, точно не вистачає.


3

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

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

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

Зараз є друга проблема - "вірулентний прихильник розвитку SOLID". Це впевнено не здається, що у вас є прекрасні стосунки з цим розробником, тому будь-які спроби переконати його / її проблеми в кодовій базі придушені. Вам потрібно буде відремонтувати відносини, щоб ви могли по-справжньому обговорити проблеми. Що я б порекомендував - це пиво.

Ні серйозно - якщо ви не п'єте голову до кав’ярні. Вийдіть з офісу і кудись розслабившись, де можна неофіційно розповісти про ці речі. Замість того, щоб намагатися виграти аргумент на засіданні, чого ви не будете, ведіть дискусію десь весело. Спробуйте визнати, що цей диявол, який виганяє вас, - це фактично функціонуюча людина, яка намагається вивести програмне забезпечення «у двері» і не хоче перевозити лайно. Оскільки ви, швидше за все, поділяєтеся спільним принципом, ви можете почати обговорювати, як вдосконалити дизайн, дотримуючись при цьому SRP.

Якщо ви можете визнати, що SRP - це добре, що ви просто інтерпретуєте аспекти по-різному, ви, ймовірно, можете почати продуктивні розмови.


-1

Я погоджуюся з вашим рішенням керівника команди [оновлення = 2012.05.31], що SRP, як правило, добре подумайте. Але я повністю згоден на коментар @ Spoike -s, що конструктор з 20 аргументами інтерфейсу - це набагато багато. [/ Update]:

Представлення SRP з IoC переносить складність від одного "класу, який відповідає за багато відповідальних", до багатьох класів srp та набагато складнішою ініціалізацією на користь

  • полегшення тестування одиниць / tdd (тестування одного класу srp за один раз)
  • але ціною
    • значно більш чіткою ініціалізацією коду та інтеграцією та
    • більш чітке налагодження
    • фрагментація (= розподіл коду на кілька файлів / каталогів)

Я боюся, що ви не зможете зменшити кодову фрагментацію, не пошкодивши srp.

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

   class MySrpClass {
      MySrpClass(Interface1 parm1, Interface2 param2, .... Interface20 param2) {
      }
   } 

   class MySyntaxSugarClass : MySrpClass {
      MySyntaxSugarClass() {
         super(new MyInterface1Implementation(), new MyImpl2(), ....)
      }
   }

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