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