Які практичні способи реалізації СРП?


11

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

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

Єдиний спосіб, який я знайшов, - це використовувати речення "The ......... повинен ......... сам". де перший пробіл - ім'я класу, а пізніше - ім'я методу (відповідальності).

Однак іноді важко зрозуміти, чи дійсно відповідальність порушує СРП.

Чи є більше способів перевірити наявність SRP?

Примітка:

Питання полягає не в тому, що означає СРП, а в практичній методології або низці кроків для перевірки та реалізації СРП.

ОНОВЛЕННЯ

Клас звіту

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

Приклад звідси .


Це цікаве правило, але ви все-таки можете написати: "Клас людини може зробити сам". Це може вважатися порушенням для SRP, оскільки включення графічного інтерфейсу в той самий клас, який містить ділові правила та збереження даних, не є нормальним. Тому я думаю, що вам потрібно додати поняття архітектурних доменів (яруси та шари) і переконатися, що це твердження дійсне лише для 1 цього домену (наприклад, GUI, доступу до даних тощо)
NoChance

@EmmadKareem Це правило згадувалося в першому об'єктно-орієнтованому аналізі та дизайні, і саме про це я подумав. Дещо не вистачає практичного способу її здійснення. Вони згадували, що інколи відповідальність не буде такою очевидною для дизайнера, і він повинен використовувати багато здорового глузду, щоб судити про те, чи дійсно цей метод повинен бути у цьому класі чи ні.
Сонго

Якщо ви дійсно хочете зрозуміти SRP, прочитайте деякі твори дядька Боб Мартіна. Його код - це одне з найкрасивіших, що я бачив, і я вірю, що все, що він каже про СРП, - це не лише обґрунтована порада, але й більше, ніж просто махання руками.
Роберт Харві

А чи не пояснить, будь ласка, пояснення, чому поліпшити посаду ?!
Songo

Відповіді:


7

НРП заперечує, що клас повинен мати колись одну причину зміни.

Деконструюючи клас "звіт" у питанні, він має три методи:

  • 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, повинен мати лише одну або дві змінні стану. Таку тонку обгортку рідко можна очікувати, що вона може зробити щось справді корисне. Тому не перестарайтеся з цим.


+1 дійсно чудова відповідь. Однак я просто розгублений у класі IncomeStatement. Є пропонований вами дизайн означає , що IncomeStatementбуде мати екземпляри IncomeStatementPrinterі IncomeStatementRendererтак , що , коли я дзвоню print()на IncomeStatementньому буде делегувати виклик IncomeStatementPrinterзамість цього?
Songo

@Songo: Абсолютно ні! Ви не повинні мати циклічних залежностей, якщо слідкуєте за 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, я думаю, але я дійсно не шанувальник.
Aaronaught

2

Я перевіряю наявність SRP - це перевірити кожен метод (відповідальність) класу і задати наступне питання:

"Чи мені колись потрібно буде змінити спосіб виконання цієї функції?"

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


1

Ось цитата з правила 8 Об’єктної гімнастики :

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

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


2
Цей погляд є безнадійно спрощеним. Навіть відоме, але просте рівняння Ейнштейна вимагає двох змінних.
Роберт Харві

Питання ОП було "Чи є більше способів перевірити наявність СРП?" - це один з можливих показників. Так, це спрощено, і воно не витримує в кожному випадку, але це один із можливих способів перевірити, чи було порушено SRP.
MattDavey

1
Я підозрюю, що стан, що змінюється, проти непорушного стану, також є важливою
увагою

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

@Dunk Я не згоден з вами, але ця дискусія зовсім поза темою для питання.
MattDavey

1

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

EDIT: Також зауважте, що клас незмінний. Тож як тільки він створений, ви нічого не можете змінити. Ви можете додати 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());
    }
}

Дякуємо за реалізацію. У мене є дві речі, в рядку if (reportData == null)я припускаю, що ви маєте на увазі dataнатомість По-друге, я сподівався знати, як ти дійшов до цієї реалізації. Як і чому ви вирішили делегувати всі дзвінки на інші об’єкти. Ще одна річ, про яку я завжди цікавився, чи справді відповідальність надрукувати сам звіт ?! Чому ви не створили окремий printerклас, який бере reportсвій конструктор?
Songo

Так, reportData = дані, вибачте за це. Делегація дозволяє здійснювати тонкий контроль над залежностями. Під час виконання ви можете надати альтернативні реалізації для кожного компонента. Тепер ви можете мати HtmlPrinter, PdfPrinter, JsonPrinter, ... і т. Д. Це також зручно для тестування, оскільки ви можете протестувати делеговані компоненти як ізольовано, так і інтегровані в об'єкт вище. Ви, звичайно, могли б перевернути зв’язок між принтером та звітом, я просто хотів показати, що можна забезпечити рішення із наданим інтерфейсом класу. Це звичка працювати над застарілими системами. :)
Хіт Ліллі

hmmmm ... Тож якби ви будували систему з нуля, який варіант ви б взяли? PrinterКлас , який приймає звіт або Reportклас , який приймає принтер? Я зіткнувся з подібною проблемою раніше, коли мені довелося розібрати звіт, і я посперечався зі своїм TL, чи слід створити аналізатор, який приймає звіт, чи повинен звіт мати аналізатор всередині нього, а parse()виклик переданий йому.
Songo

Я б зробив як ... printer.print (звіт), щоб запустити, а 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 - це зануритися в кодову базу і відскочити біль.


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

Оновлено. SRP залежить від контексту, якщо ви розмістили весь клас (в окремому запитанні), це було б легше пояснити.
Гарретт Холл

Дякуємо за оновлення. Питання, однак, чи справді відповідальність надрукувати сам звіт ?! Чому ви не створили окремий клас принтера, який бере звіт у своєму конструкторі?
Songo

Я просто кажу, що SRP залежить від самого коду, ви не повинні застосовувати його догматично.
Гарретт Холл

так, я розумію. Але якби ви будували систему з нуля, який варіант ви б прийняли? PrinterКлас , який приймає звіт або Reportклас , який приймає принтер? Я багато разів стикався з таким дизайнерським питанням, перш ніж з'ясувати, чи виявиться код складним чи ні.
Songo

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