Принцип на единствена отговорност, също известен като принципа на единна отговорност,
също така е принцип на единна променливост — изключително трудно разбираем за интервюиращия и доста деликатен въпрос на интервюто за програмисти.
Първото сериозно запознанство с този принцип за мен се случи в началото на първия курс, когато младите и неопитни бяхме отведени в гората, за да направим от личинките студенти — истински студенти.
В гората бяхме разделени на групи от 8-9 души и организирахме състезание — коя група ще изпие бутилка водка по-бързо, при условие, че първият човек от групата налива водка в чашата, вторият изпива, а третият похапва. Извършилият своята операция единица заема място в края на опашката на групата.
Случай, когато размерът на опашката е кратен на три, беше добра реализация на SRP.
Определение 1. Единна отговорност.
Официалното определение на принципа за единна отговорност (SRP) гласи, че всеки обект има своя отговорност и причина за съществуване, и тази отговорност е само една.
Нека да разгледаме обекта „Випивоха“ (Tippler).
За да изпълним принципа на SRP, нека разделим отговорностите на трима:
- Един налива (PourOperation)
- Един изпива (DrinkUpOperation)
- Един похапва (TakeBiteOperation)
Всеки от участниците в процеса е отговорен за една компонента от процеса, тоест има една атомна отговорност — да изпие, да налее или да похапне.
Випивохата, от своя страна, е фасад за тези операции:
class Tippler {
//...
void Act(){
_pourOperation.Do() // наливам
_drinkUpOperation.Do() // изпивам
_takeBiteOperation.Do() // похапвам
}
}
Защо?
Човек-програмист пише код за човек-маймуна, а човек-маймуна е невнимателен, глупав и винаги бърза. Той може да запомни и разбере около 3 — 7 термина в един момент.
В случая на випивохата, тези термини са три. Обаче, ако напишем кода в една дълга редица, ще се появят ръце, чаши, побоища и безкрайни спорове за политика. И всичко това ще бъде в тялото на един метод. Убеден съм, че сте виждали такъв код в практиката си. Не е най-човечният тест за психика.
От друга страна, човек-обезьяна е заточен на моделирането на обекти от реалния свят в главата си. Въображението му му позволява да сблъсква, събира нови обекти и също така да ги разглобява. Представете си стара модел на кола. Можете да си представите как отваряте вратата, сваляте облицовката на вратата и виждате механизми за стъклодърпачите, вътре в които има зъбни колела. Но не можете да видите всички компоненти на колата едновременно, в едно "листинг". Най-малкото „човек-обезяна” не може.
Затова програмистите декомпозирали сложни механизми на набор от по-малко сложни и функциониращи елементи. Въпреки това, декомпозицията може да се извърши по различен начин: в много стари коли - въздуховодът преминава в вратата, а в съвременните - повреда на електрониката на ключалката пречи на двигателя да се стартира, което създава проблеми при ремонта.
Така че, SRP е принцип, който обяснява КАК да декомпозираш, т.е. къде да проведеш линията на разделение..
Той казва, че декомпозиране трябва да се прави по принципа на разделяне на "отговорности", т.е. по задачи на съответните обекти.
Нека се върнем към пияницата и плюсовете, които получава човек-обезяна при декомпозиране:
- Кодът стана изключително ясен на всеки ниво.
- Код може да бъде написан от няколко програмисти едновременно (всеки пише отделен елемент).
- Улеснено е автоматичното тестване - колкото по-прост е елементът, толкова по-лесно е да го тествате.
- Появява се композиционност на кода - можете да заменяте DrinkUpOperation с операция, при която пияницата излива течност под масата. Или да замените операцията за наливане с операция, при която смесвате вино и вода или водка и бира. В зависимост от изискванията на бизнеса, можете да правите всичко, без да пипате кода на метода. Tippler.Act.
- От тези операции можете да създадете обжора (използвайки само TakeBitOperation), Алкохолик (използвайки само DrinkUpOperation напряко от бутилката) и да удовлетворите много други бизнес изисквания.
(Ой, изглежда това вече е принципът OCP и наруших отговорността на този пост)
И, разбира се, минусите:
- Ще се наложи да създадете повече типове.
- Пияницата ще пие за първи път с няколко часа по-късно, отколкото би могъл.
Определение 2. Единна изменчивост.
Момент, господа! Класът на пияницата е отговорен за една единствена задача — да пие! И изобщо, терминът „отговорност“ е много разтегливо понятие. Някой е отговорен за съдбата на човечеството, а друг е отговорен за вдигането на обърнатите на полюса пингвини.
Нека разгледаме две реализации на пияницата. Първата, посочена по-горе, съдържа три класа — наливане, изпиване и закусване.
Втората, написана по методологията „Напред и само напред“, съдържа цялата логика в метода Act:
//Не тратьте время на изучение этого класса. Лучше съешьте печеньку
сlass BrutTippler {
//...
void Act(){
// наливаем
if(!_hand.TryDischarge(from:_bottle, to:_glass, size:_glass.Capacity))
throw new OverdrunkException();
// выпиваем
if(!_hand.TryDrink(from: _glass, size: _glass.Capacity))
throw new OverdrunkException();
//Закусываем
for(int i = 0; i< 3; i++){
var food = _foodStore.TakeOrDefault();
if(food==null)
throw new FoodIsOverException();
_hand.TryEat(food);
}
}
}И двата тези класа, от гледна точка на външен наблюдател, изглеждат абсолютно идентични и изпълняват единствената отговорност „да пият“.
Конфузия!
Тогава се обръщаме към интернет и научаваме друго определение на SRP — Принцип на единствената изменчивост (Single Changeability Principle).
SCP казва, че „Модулът има една единствена причина за промяна“. Тоест „Отговорността е причината за промяна“.
(Изглежда, че момчетата, измислили първоначалното определение, бяха уверени в телепатичните способности на човека-обезяна)
Сега всичко си идва на мястото. Отделно можем да променяме процедурите за наливане, изпиване и закусване, а в самата пияница можем да променим само последователността и състава на операциите, например, да преместим закуската преди изпиването или да добавим четене на тост.
В подхода „Напред и само напред“, всичко, което може да се промени — се променя само в метода Act. Това може да е четливо и ефикасно, когато логиката е малка и рядко се променя, но често това приключва с ужасни методи по 500 реда всеки, с количество if-ове, по-голямо от необходимото за приемането на Русия в НАТО.
Определение 3. Локализация на промените.
Пияниците често не разбират защо са се събудили в чужд апартамент, или къде е мобилният им телефон. Време е да добавим подробна логировка.
Нека започнем логировката с процеса на наливане:
class PourOperation: IOperation{
PourOperation(ILogger log /*....*/){/*...*/}
//...
void Do(){
_log.Log($"Преди наливането с {_hand} и {_bottle}");
//Бизнес логика на наливането ...
_log.Log($"След наливането с {_hand} и {_bottle}");
}
}Инкапсулирайки я в PourOperation, ние постъпихме разумно от гледна точка на отговорността и инкапсулацията, но с принципа за изменчивост сега имаме конфуз. Освен самата операция, която може да се променя, изменчива става и самото логване. Ще трябва да разделим и да направим специален логер за операцията по наливане:
interface IPourLogger{
void LogBefore(IHand, IBottle){}
void LogAfter(IHand, IBottle){}
void OnError(IHand, IBottle, Exception){}
}
class PourOperation: IOperation{
PourOperation(IPourLogger log /*....*/){/*...*/}
//...
void Do(){
_log.LogBefore(_hand, _bottle);
try{
//... бизнес логика
_log.LogAfter(_hand, _bottle);
}
catch(exception e){
_log.OnError(_hand, _bottle, e);
}
}
}Внимателният читател ще забележи, че LogAfter, LogBefore и OnError също могат да се променят поотделно, и по аналогия с предишните действия ще създаде три класа: PourLoggerBefore, PourLoggerAfter и PourErrorLogger.
А припомняйки, че операциите за наливане са три — получаваме девет класа за логване. В крайна сметка целият наливател се състои от 14 (!!!) класа.
Хипербола? Вряд ли! Човек-обезьяна с декомпозиционна граната ще раздроби “наливателя” на графин, чаша, оператори за наливане, услуга за подаване на вода, физическа модел на сблъсък на молекули и през следващото тримесечие ще се опитва да разплита зависимости без глобални променливи. И повярвайте — той няма да спре.
Точно в този момент много хора стигат до извода, че SRP — това са приказки от розовите кралства и се отдават на измислени истории...
… така и не узнавайки за съществуването на третото определение на SRP:
„Принципът на единствената отговорност гласи, че сходните за промяна неща трябва да се съхраняват на едно място“. или “Това, което се променя заедно, трябва да се съхранява на едно място”
Тоест, ако променяме логването на операцията, то трябва да го променяме на едно място.
Това е много важен момент — тъй като всички обяснения на SRP, които бяха по-горе, говореха за това, че трябва да се разчупват типовете, докато те се разчупват, това налагаше „лимит отгоре“ на размера на обекта, а сега вече говорим и за „лимит отдолу“. С други думи, SRP не само изисква „разчупвай, докато се разчупва“, но и да не се прекаляваме — „да не се раздробяват свързани неща“. Това е великата битка на бръснача на Оккам с човека-обезьяна!
Сега на наливателя трябва да му стане по-лесно. Освен това, че не е нужно да се разчупва логерът IPourLogger на три класа, можем също да обединим всички логери в един тип:
class OperationLogger{
public OperationLogger(string operationName){
/*..*/}
public void LogBefore(object[] args){
/*...*/}
public void LogAfter(object[] args){
/*..*/}
public void LogError(object[] args, exception e){
/*..*/}
}И ако ни се добави четвърти тип операция, системата за логиране вече е готова. Кодът на самите операции е чист и свободен от инфраструктурен шум.
В резултат на това имаме 5 класа, които решават задачата за изсипване:
- Операция на наливане
- Операция на изсипване
- Операция на похапване
- Логер
- Фасада на пияницата
Всеки от тях отговаря строго за една функционалност и има само една причина за промяна. Всички сходни правила за промяна са събрани заедно.
Пример от реалния живот
Веднъж писахме сервиз за автоматична регистрация на b2b клиент. И се появи GOD метод с 200 реда подобно съдържание:
- Отиди в 1С и създай сметка
- С тази сметка отиди при модула за плащания и я регистрирай там
- Провери дали сметката не е създадена в главния акаунт сървър
- Създай нов акаунт
- Резултатът от регистрацията в модула за плащания и номерът на 1С добави в сервиза за резултати от регистрацията
- Добави информация за акаунта в тази таблица
- Създай номер на точка за този клиент в сервиза за точки. Предай на този сервиз номера на сметката в 1С.
И в този списък имаше още около 10 бизнес операции с ужасна свързаност. Обектът на сметката беше нужен почти на всички. Идентификаторът на точката и името на клиента бяха нужни в половината повиквания.
След час рефакториране, успяхме да отделим инфраструктурния код и някои нюанси на работа с акаунта в отделни методи/класове. God методът стана по-лек, но останаха 100 реда код, които отказваха да се разплетат.
Само след няколко дни дойде осъзнаването, че същността на този „пораснал“ метод — е бизнес алгоритъм. И че първоначалното описание на ТЗ беше доста сложно. И именно опитът да се разчупи този метод на парчета щеше да бъде нарушение на SRP, а не обратното.
Формализъм.
Време е да оставим нашата пияница на мира. Избършете сълзите — задължително ще се върнем при него някой ден. А сега нека формализираме знанията от тази статия.
Формализъм 1. Определение на SRP
- Разделяйте елементите така, че всеки от тях да бъде отговорен за нещо едно.
- Отговорността е дефинирана като „причина за промяна“. Тоест всеки елемент има само една причина за промяна в термини на бизнес логика.
- Потенциални изменения в бизнес логиката трябва да бъдат локализирани. Променяемите синхронно елементи трябва да бъдат наблизо.
Формалистичност 2. Необходими критерии за самооценка.
Не съм срещал достатъчни критерии за изпълнение на SRP. Но има необходими условия:
1) Поставете си въпроса — какво прави този клас/метод/модул/услуга. Трябва да можете да отговорите на него с просто определение. ( Благодаря )
обяснения
Впрочем, понякога е много трудно да се намери просто определение.
2) Поправка на някаква грешка или добавяне на нова функционалност засяга минимален брой файлове/класове. В идеала — един.
обяснения
Тъй като отговорността (за функционалността или бага) е инкапсулирана в един файл/клас, вие точно знаете къде да търсите и какво да поправяте. Например: функционалността за промяна на логването на операциите ще изисква да промените само логера. Не е необходимо да обикаляте около целия друг код.
Друг пример — добавянето на нов UI контрол, подобен на предишните. Ако това ви принуждава да добавите 10 различни ентитети и 15 различни конвертора — изглежда, че сте "предробили".
3) Ако няколко разработчици работят над различни функционалности на вашия проект, вероятността от конфликт при сливане — т.е. вероятността един и същ файл/клас да бъде променен от няколко разработчици едновременно — е минимална.
обяснения
Ако при добавяне на нова операция "Излейте водка под масата" е необходимо да се докоснете до логера, операцията на изпиване и изливане — изглежда, че отговорностите са разделени неправилно. Разбира се, това не винаги е възможно, но трябва да се опитвате да намалите този показател.
4) При уточняващ въпрос за бизнес логиката (от разработчик или мениджър) прониквате строго в един клас/файл и получавате информация само от него.
обяснения
Функционалности, правила или алгоритми компактно написани, всяка на едно място, а не разпръснати с флагове из цялото пространство на кода.
5) Именуването е ясно.
обяснения
Нашият клас или метод е отговорен за нещо едно, и отговорността е отразена в името му.
AllManagersManagerService — вероятно, God клас.
LocalPayment — вероятно, не.
Формалистичност 3. Методология на разработка „Оккам-първо”.
В началото на проектирането, човек-обезьяна не знае и не чувства всички нюанси на решаваната задача и може да допусне грешки. Можете да грешите по различни начини:
- Създаването на прекалено големи обекти, като се съединят различни отговорности
- Разделяне на единствена отговорност на много различни типове
- Неправилно определяне на границите на отговорността
Важно е да запомните правилото: "по-добре е да се греши в голяма степен" или "не сте сигурни – не дробите". Например, ако вашият клас обединява две отговорности – той все още е разбираем и може да бъде разделен на два с минимални изменения в клиентския код. Събирането на парчета стъкло обратно в чаша обикновено е по-трудно заради размазан контекст в няколко файла и липса на необходимите зависимости в клиентския код.
Време е да се завърши
Областта на приложение на SRP не се ограничава до ООП и SOLID. Той е приложим към методи, функции, класове, модули, микросервизи и услуги. Той е приложим както за "фигакс-фигакс-и-в-прод", така и за "рокет-сайнс" разработка, винаги правейки света малко по-добър. Ако се замислите, това е едва ли не основен принцип на цялата инженерия. Машиностроене, системи за управление и изобщо всички сложни системи – се изграждат от компоненти, а "недопрекия" лишава строителите от гъвкавост, "препрекия" – от ефективност, а неправилните граници – от разум и душевен мир.
SRP не е създаден от природата и не е част от точната наука. Той произлиза от нашите биологични и психологически ограничения. Това е просто начин да контролираме и развиваме сложни системи чрез човешкия мозък. Той ни показва как да декомпозираме система. Първоначалната формулировка изискваше значителен опит в телепатията, но се надявам, че тази статия донякъде е разсеяла мъглата.
Източник: habr.com
