Принцип на единствена одговорност. Не толку едноставно како што изгледа

Принцип на единствена одговорност. Не толку едноставно како што изгледа Принцип на единствена одговорност, познат и како принцип на единствена одговорност,
ака принципот на униформа варијабилност - екстремно лизгав тип за разбирање и толку нервозно прашање на интервју со програмер.

Моето прво сериозно запознавање со овој принцип се случи на почетокот на првата година, кога младите и зелените беа однесени во шумата за да направат ученици од ларви - вистински студенти.

Во шумата бевме поделени во групи од по 8-9 луѓе и имавме натпревар - која група најбрзо ќе пие шише вотка, под услов првиот од групата да истури вотка во чаша, вториот да ја испие, а третиот има закуска. Единицата што ја завршила својата работа се движи до крајот на редот на групата.

Случајот кога големината на редот беше повеќекратно од три беше добра имплементација на SRP.

Дефиниција 1. Единствена одговорност.

Официјалната дефиниција на Принципот на единствена одговорност (СРП) вели дека секој субјект има своја одговорност и причина за постоење, а има само една одговорност.

Размислете за објектот „Пијач“ (Типлер).
За да го имплементираме принципот SRP, ќе ги поделиме одговорностите на три:

  • Еден истура (PourOperation)
  • Еден пие (DrinkUpOperation)
  • Еден има закуска (TakeBiteOperation)

Секој од учесниците во процесот е одговорен за една компонента на процесот, односно има една атомска одговорност - да пие, истури или грицка.

Дупката за пиење, пак, е фасада за овие операции:

сlass Tippler {
    //...
    void Act(){
        _pourOperation.Do() // налить
        _drinkUpOperation.Do() // выпить
        _takeBiteOperation.Do() // закусить
    }
}

Принцип на единствена одговорност. Не толку едноставно како што изгледа

Зошто?

Човечкиот програмер пишува код за човекот-мајмун, а човекот-мајмун е невнимателен, глупав и секогаш брза. Тој може да држи и разбере околу 3 - 7 термини одеднаш.
Во случај на пијаница, постојат три од овие термини. Меѓутоа, ако го напишеме кодот со еден лист, тогаш тој ќе содржи раце, очила, тепачки и бескрајни расправии за политика. И сето тоа ќе биде во телото на еден метод. Сигурен сум дека сте виделе таков код во вашата пракса. Не е најхуманиот тест за психата.

Од друга страна, човекот мајмун е дизајниран да симулира објекти од реалниот свет во неговата глава. Во својата имагинација, тој може да ги турка заедно, да составува нови предмети од нив и да ги расклопува на ист начин. Замислете автомобил од стар модел. Во вашата имагинација, можете да ја отворите вратата, да ја одвртите облогата на вратата и да ги видите механизмите за подигање на прозорците, во кои ќе има запчаници. Но, не можете да ги видите сите компоненти на машината во исто време, во еден „список“. Барем „човекот мајмун“ не може.

Затоа, човечките програмери ги разложуваат сложените механизми во збир на помалку сложени и работни елементи. Сепак, може да се разложи на различни начини: во многу стари автомобили, воздушниот канал влегува во вратата, а кај модерните автомобили, дефект во електрониката на бравата го спречува палењето на моторот, што може да биде проблем за време на поправките.

Сега, SRP е принцип кој објаснува КАКО да се разложи, односно каде да се повлече линијата на поделба.

Тој вели дека е неопходно да се разложи според принципот на поделба на „одговорноста“, односно според задачите на одредени предмети.

Принцип на единствена одговорност. Не толку едноставно како што изгледа

Да се ​​вратиме на пиењето и на предностите што ги добива човекот мајмун при распаѓање:

  • Кодот стана исклучително јасен на секое ниво
  • Кодот може да го напишат неколку програмери одеднаш (секој пишува посебен елемент)
  • Автоматското тестирање е поедноставено - колку е поедноставен елементот, толку е полесно да се тестира
  • Се појавува составноста на кодот - можете да го замените DrinkUpOperation на операција во која пијаница истура течност под масата. Или заменете ја операцијата на истурање со операција во која мешате вино и вода или вотка и пиво. Во зависност од деловните барања, можете да направите сè без да го допирате кодот на методот Типлер.Акт.
  • Од овие операции можете да го свиткате лакомот (само со употреба TakeBitOperation), Алкохолни (се користи само DrinkUpOperation директно од шишето) и исполнуваат многу други деловни барања.

(О, се чини дека ова е веќе принцип на OCP, и јас ја прекршив одговорноста на овој пост)

И, се разбира, лошите страни:

  • Ќе треба да создадеме повеќе типови.
  • Еден пијаница за прв пат пие неколку часа подоцна отколку што инаку би пиел.

Дефиниција 2. Унифицирана варијабилност.

Дозволете ми, господа! Класата за пиење има и единствена одговорност - пие! И воопшто, зборот „одговорност“ е крајно нејасен концепт. Некој е одговорен за судбината на човештвото, а некој е одговорен за одгледување на пингвините кои беа превртени на столбот.

Да разгледаме две имплементации на пијачот. Првиот, споменат погоре, содржи три класи - точење, пиење и закуска.

Вториот е напишан преку методологијата „Напред и само напред“ и ја содржи целата логика во методот Дејствува:

//Не тратьте время  на изучение этого класса. Лучше съешьте печеньку
с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 - Принципот на единствена променливост.

СКП наведува дека „Модулот има една и единствена причина за промена“. Тоа е, „Одговорноста е причина за промена“.

(Се чини дека момците кои дојдоа до оригиналната дефиниција беа сигурни во телепатските способности на човекот мајмун)

Сега се си доаѓа на свое место. Посебно, можеме да ги смениме процедурите за точење, пиење и грицкање, но во самиот пијалок можеме само да го промениме редоследот и составот на операциите, на пример, со поместување на закуската пред да пиеме или додавајќи читање на тост.

Во пристапот „Напред и само напред“, сè што може да се промени се менува само во методот Дејствува. Ова може да биде читливо и ефективно кога има малку логика и ретко се менува, но често завршува со ужасни методи од по 500 линии, со повеќе ако-изјави отколку што е потребно за Русија да влезе во НАТО.

Дефиниција 3. Локализација на промените.

Оние кои пијат често не разбираат зошто се разбудиле во туѓ стан или каде им е мобилниот телефон. Време е да се додаде детална евиденција.

Да почнеме да се најавуваме со процесот на истурање:

class PourOperation: IOperation{
    PourOperation(ILogger log /*....*/){/*...*/}
    //...
    void Do(){
        _log.Log($"Before pour with {_hand} and {_bottle}");
        //Pour business logic ...
        _log.Log($"After pour with {_hand} and {_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{
             //... business logic
             _log.LogAfter(_hand, _bottle");
        }
        catch(exception e){
            _log.OnError(_hand, _bottle, e)
        }
    }
}

Педантниот читател ќе го забележи тоа LogAfter, Најавете се пред и OneError може да се менува и поединечно и, по аналогија со претходните чекори, ќе се создадат три класи: PourLoggerBefore, PourLoggerAfter и PourErrorLogger.

И запомнувајќи дека има три операции за пијач, добиваме девет часови за сеча. Како резултат на тоа, целиот круг за пиење се состои од 14 (!!!) класи.

Хипербола? Тешко! Човек мајмун со граната за распаѓање ќе го подели „точачот“ на декантер, чаша, оператори за точење, сервис за водоснабдување, физички модел на судир на молекулите и следната четвртина ќе се обиде да ги отплетка зависностите без глобални променливи. И верувајте ми, тој нема да запре.

Токму во овој момент многумина доаѓаат до заклучок дека СРП се бајки од розовите кралства и заминуваат да играат нудли...

... без воопшто да научиме за постоењето на трета дефиниција за Срп:

„Начелото за единствена одговорност го вели тоа работи кои се слични на промена треба да се складираат на едно место“. или "Она што се менува заедно треба да се чува на едно место"

Односно, ако го промениме логирањето на некоја операција, тогаш мора да го промениме на едно место.

Ова е многу важен момент - бидејќи сите објаснувања на 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 клиент. И се појави ГОД метод за 200 реда со слична содржина:

  • Одете на 1C и креирајте сметка
  • Со оваа сметка, одете во модулот за плаќање и креирајте го таму
  • Проверете дали сметката со таква сметка не е креирана на главниот сервер
  • Направете нова сметка
  • Додадете ги резултатите од регистрацијата во модулот за плаќање и бројот 1c во услугата за резултати од регистрацијата
  • Додадете информации за сметката на оваа табела
  • Направете број на точка за овој клиент во услугата за точки. Пренесете го бројот на вашата сметка 1c на оваа услуга.

И имаше уште околу 10 деловни операции на оваа листа со страшна поврзаност. Речиси на сите им требаше објектот на сметката. ИД на точката и името на клиентот беа потребни во половина од повиците.

По еден час рефакторирање, успеавме да го раздвоиме инфраструктурниот код и некои нијанси за работа со сметка во посебни методи/класи. Методот на Бог го олесни, но останаа 100 линии код што едноставно не сакаше да се отплетка.

Само по неколку дена стана јасно дека суштината на овој „лесен“ метод е деловен алгоритам. И дека оригиналниот опис на техничките спецификации бил доста сложен. И тоа е обидот да се скрши овој метод на парчиња што ќе го наруши SRP, а не обратно.

Формализам.

Време е да ги оставиме нашите пијани на мира. Исушете ги солзите - ние дефинитивно ќе се вратиме на тоа некогаш. Сега ајде да го формализираме знаењето од овој напис.

Формализам 1. Дефиниција на SRP

  1. Одделете ги елементите така што секој од нив е одговорен за една работа.
  2. Одговорноста значи „причина за промена“. Односно, секој елемент има само една причина за промена, во однос на деловната логика.
  3. Потенцијални промени во деловната логика. мора да биде локализиран. Елементите што се менуваат синхроно мора да се во близина.

Формализам 2. Потребни критериуми за самотестирање.

Не видов доволно критериуми за исполнување на СРП. Но, постојат неопходни услови:

1) Запрашајте се што прави оваа класа/метод/модул/услуга. мора да одговорите со едноставна дефиниција. ( Ви благодарам Брајтори )

објаснувања

Сепак, понекогаш е многу тешко да се најде едноставна дефиниција

2) Поправањето на грешка или додавањето нова функција влијае на минималниот број датотеки/класи. Идеално - еден.

објаснувања

Бидејќи одговорноста (за карактеристика или грешка) е инкапсулирана во една датотека/класа, точно знаете каде да барате и што да уредувате. На пример: карактеристиката за промена на излезот од операциите за евидентирање ќе бара менување само на логерот. Нема потреба да поминувате низ остатокот од кодот.

Друг пример е додавање на нова контрола на UI, слична на претходните. Ако ова ве принуди да додадете 10 различни ентитети и 15 различни конвертори, изгледа како да претерате.

3) Ако неколку програмери работат на различни карактеристики на вашиот проект, тогаш веројатноста за конфликт на спојување, односно веројатноста дека истата датотека/класа ќе биде изменета од неколку програмери истовремено, е минимална.

објаснувања

Ако при додавање на нова операција „Сијте вотка под маса“, треба да влијаете на дрвосечачот, работата на пиење и точење, тогаш изгледа дека одговорностите се криво поделени. Се разбира, тоа не е секогаш можно, но треба да се обидеме да ја намалиме оваа бројка.

4) Кога ќе ви биде поставено појаснувачко прашање за деловната логика (од развивач или менаџер), вие влегувате строго во една класа/датотека и добивате информации само од таму.

објаснувања

Карактеристиките, правилата или алгоритмите се напишани компактно, секој на едно место и не се расфрлани со знаменца низ просторот на кодот.

5) Именувањето е јасно.

објаснувања

Нашата класа или метод е одговорна за една работа, а одговорноста се рефлектира во нејзиното име

AllManagersManagerService - најверојатно класа на Бог
LocalPayment - веројатно не

Формализам 3. Occam-first развојна методологија.

На почетокот на дизајнот, човекот мајмун не ги знае и не ги чувствува сите суптилностите на проблемот што се решаваат и може да направи грешка. Можете да направите грешки на различни начини:

  • Направете ги предметите премногу големи со спојување на различни одговорности
  • Реформирање со поделба на една одговорност на многу различни типови
  • Неправилно дефинирајте ги границите на одговорноста

Важно е да се запамети правилото: „подобро е да се направи голема грешка“ или „ако не сте сигурни, не ја разделувајте“. Ако, на пример, вашата класа содржи две одговорности, тогаш таа сепак е разбирлива и може да се подели на две со минимални промени во кодот на клиентот. Составувањето стакло од парчиња стакло обично е потешко поради контекстот што се шири низ неколку датотеки и недостатокот на потребните зависности во кодот на клиентот.

Време е да го наречеме ден

Опсегот на SRP не е ограничен на OOP и SOLID. Тоа се однесува на методи, функции, класи, модули, микросервиси и услуги. Тоа се однесува и на развојот на „figax-figax-and-prod“ и на „racket-science“, што го прави светот малку подобар насекаде. Ако размислите за тоа, ова е речиси основниот принцип на целото инженерство. Машинското инженерство, контролните системи и навистина сите сложени системи се изградени од компоненти, а „подфрагментацијата“ ги лишува дизајнерите од флексибилност, „префрагментацијата“ ги лишува дизајнерите од ефикасност, а неправилните граници ги лишуваат од разумот и мирот на умот.

Принцип на единствена одговорност. Не толку едноставно како што изгледа

SRP не е измислен од природата и не е дел од егзактната наука. Излегува од нашите биолошки и психолошки ограничувања.Тоа е само начин да се контролираат и развијат сложените системи со помош на мозокот на човек од мајмун. Тој ни кажува како да разложиме систем. Оригиналната формулација бараше прилично телепатија, но се надевам дека овој напис ќе исчисти дел од димната завеса.

Извор: www.habr.com

Додадете коментар