Single Responsibility Principle. Не такі просты, як здаецца

Single Responsibility Principle. Не такі просты, як здаецца Single responsibility principle, ён жа прынцып адзінай адказнасці,
ён жа прынцып адзінай зменлівасці - вельмі слізкі для разумення хлопец і гэтак нервовае пытанне на сумоўі праграміста.

Першае сур'ёзнае знаёмства з гэтым прынцыпам адбылося для мяне ў пачатку першага курса, калі маладых і зялёных нас вывезлі ў лес, каб зрабіць з лічынак студэнтаў - студэнтаў сапраўдных.

У лесе нас падзялілі на групы па 8-9 чалавек у кожнай і зладзілі спаборніцтва - якая група хутчэй вып'е бутэльку гарэлкі пры ўмове, што першы чалавек з групы налівае гарэлку ў шклянку, другі выпівае, а трэці закусвае. Які выканаў сваю аперацыю юніт устае ў канец чаргі групы.

Выпадак, калі памер чаргі быў крацены тром, і з'яўляўся добрай рэалізацыяй SRP.

Азначэнне 1. Адзіная адказнасць.

Афіцыйнае вызначэнне прынцыпу адзінай адказнасці (SRP) сведчыць аб тым, што ў кожнага аб'екта ёсць свая адказнасць і прычына існавання і гэтая адказнасць у яго толькі адна.

Разгледзім аб'ект «Выпівака» (Чайнік).
Для выканання прынцыпу SRP падзелім абавязкі на траіх:

  • Адзін налівае (PourOperation)
  • Адзін выпівае (DrinkUpOperation)
  • Адзін закусвае (TakeBiteOperation)

Кожны з удзельнікаў працэсу адказны за адну кампаненту працэсу, гэта значыць мае адну атамарную адказнасць - выпіць, наліць або закусіць.

Выпівака ж, у сваю чаргу з'яўляецца фасадам для дадзеных аперацый:

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

Single Responsibility Principle. Не такі просты, як здаецца

Навошта?

Чалавек-праграміст піша код для чалавека-малпы, а чалавек-малпа няўважлівы, дурны і вечна некуды спяшаецца. Ён можа ўтрымаць і зразумець каля 3 - 7 тэрмаў у адзін момант часу.
У выпадку выпівакі гэтых тэрмоў тры. Аднак калі мы напішам код адной прасцінай, то ў ім з'явяцца рукі, шклянкі, мардабой і бясконцыя спрэчкі аб палітыцы. І ўсё гэта будзе ў целе аднаго метаду. Упэўнены - вы бачылі такі код у сваёй практыцы. Не самае гуманнае выпрабаванне для псіхікі.

З іншага боку, чалавек-малпа заменчаны на мадэляванне аб'ектаў рэальнага свету ў сваёй галаве. У сваім уяўленні ён можа іх сутыкаць, збіраць з іх новыя аб'екты і сапраўды гэтак жа разбіраць. Уявіце сабе старую мадэль машыны. Вы можаце ва ўяўленні адкрыць дзверы, адкруціць ашалёўку дзвярэй і ўбачыць там механізмы шклапад'ёмнікаў, усярэдзіне якіх будуць шасцярні. Але вы не можаце ўбачыць усе кампаненты машыны адначасова, у адным «лістынгу». Прынамсі "чалавек-малпа" не можа.

Таму людзі-праграмісты дэкампазуюць складаныя механізмы на набор менш складаных і якія працуюць элементаў. Аднак, дэкампазіраваць можна па-рознаму: у шматлікіх старых машынах паветравод выходзіць у дзверы, а ў сучасных збой электронікі замка не дае запусціцца рухавіку, што дастаўляе пры рамонце.

Дык вось, SRP - гэта прынцып, які тлумачыць ЯК дэкампазіраваць, гэта значыць дзе правесці лінію падзелу.

Ён кажа, што дэкампазіраваць трэба па прынцыпе падзелу "адказнасці", гэта значыць па задачах тых ці іншых аб'ектаў.

Single Responsibility Principle. Не такі просты, як здаецца

Вернемся да выпівакі і плюсаў, якія атрымлівае чалавек-малпачка пры дэкампазіраванні:

  • Код стаў лімітава ясны на кожным узроўні
  • Код могуць пісаць некалькі праграмістаў адразу (кожны піша асобны элемент)
  • Спрашчаецца аўтаматычнае тэставанне - чым прасцей элемент, тым лягчэй яго тэставаць
  • З'яўляецца кампазіцыйнасць кода - можна замяніць DrinkUpOperation на аперацыю, у якой выпівака вылівае вадкасць пад стол. Або замяніць аперацыю налівання на аперацыю, у якой вы мяшаеце віно і ваду або гарэлку і піва. У залежнасці ад патрабаванняў бізнэсу вы можаце ўсё, пры гэтым не чапаючы код метаду Tippler.Act.
  • З гэтых аперацый вы можаце скласці абжору ( выкарыстоўваючы толькі 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 – Прынцып адзінай зменлівасці (Single Changeability Principle).

SCP абвяшчае, што «У модуля ёсць адна і толькі адна нагода для змены“. Гэта значыць «Адказнасць - гэта нагода для змены».

(Падобна на тое, хлопцы, якія прыдумалі першапачатковае вызначэнне былі ўпэўненыя ў тэлепатычных здольнасцях чалавека-малпы)

Цяпер усё ўстае на свае месцы. Асобна можна змяняць працэдуры налівання, выпіванні і закусванні, а ў самім выпіваку мы можам памяняць толькі паслядоўнасць і склад аперацый, напрыклад, перамясціўшы закуску перад выпіваннем ці дадаўшы чытанне тоста.

У падыходзе "Наперад і толькі наперад", усё што можна памяняць - змяняецца толькі ў метадзе Дзейнічаць. Гэта можа быць чытэльна і эфектыўна ў выпадку, калі логікі трохі і яна рэдка змяняецца, але часцяком гэта канчаецца жудаснымі метадамі па 500 радкоў у кожным, з колькасцю if -ов вялікім, чым патрабуецца для ўступлення Расіі ў нато.

Азначэнне 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, LogBefore и OnError таксама могуць мяняцца па асобнасці, і па аналогіі з папярэднімі дзеяннямі створыць тры класы: PourLoggerBefore, PourLoggerAfter и PourErrorLogger.

А успомніўшы, што аперацый для выпівакі тры - атрымліваем дзевяць класаў лагавання. У выніку ўвесь выпівака складаецца з 14 (!!!) класаў.

Гіпербала? Ці ледзь! Чалавек-малпачка з дэкампазіцыйнай гранатай раздробіць "налівальніка" на графін, шклянку, аператары налівання, сэрвіс падачы вады, фізічную мадэль сутыкнення малекул і наступны квартал будзе спрабаваць разблытаць залежнасці без глабальных зменных. І паверце - ён не спыніцца.

Менавіта на гэтым моманце многія прыходзяць да высновы, што SRP – гэта казкі з ружовых каралеўстваў, і сыходзяць віць локшыну…

… так і не даведаўшыся пра існаванне трэцяга азначэння Srp:

«Прынцып адзінай адказнасці абвяшчае, што падобныя для змены рэчы павінны захоўвацца ў адным месцы“. ці “Тое, што змяняецца разам, павінна захоўвацца ў адным месцы"

Гэта значыць, калі мы мяняем лагіроўку аперацыі, то мы павінны гэта мяняць у адным месцы.

Гэта вельмі важны момант – бо ўсе тлумачэнні SRP, якія былі вышэй, казалі аб тым, што трэба драбніць тыпы, пакуль яны дробняцца, гэта значыць накладвала "абмежаванне зверху" на памер аб'екта, а зараз мы гаворым ужо і аб "абмежаванні знізу" . Іншымі словамі, SRP не толькі патрабуе "драбніць пакуль дробніцца", але і не перастарацца - "не раздрабніць счэпленыя рэчы". Гэта вялікая бітва брытвы Оккама з чалавекам-малпай!

Single Responsibility Principle. Не такі просты, як здаецца

Цяпер выпіваку павінна стаць лягчэй. Акрамя таго, што не трэба драбніць лагіроўшчык 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

  1. Падзяляйце элементы так, каб кожны з іх быў адказны за нешта адно.
  2. Адказнасць расшыфроўваецца як "падстава для змены". Гэта значыць кожны элемент мае толькі адну нагоду для змены, у тэрмінах бізнес логікі.
  3. Патэнцыйныя змены бізнес логікі. павінны быць лакалізаваны. Змяняныя сінхронна элементы павінны быць побач.

Фармалізм 2. Неабходныя крытэры самаправеркі.

Мне не сустракаліся дастатковыя крытэры выканання SRP. Але ёсць неабходныя ўмовы:

1) Задайце сабе пытанне - што робіць гэты клас / метад / модуль / сэрвіс. вы павінны адказаць на яго простым вызначэннем. ( дзякую Brightori )

тлумачэнні

Зрэшты часам падабраць простае азначэнне вельмі складана

2) Фікс некаторага бага або даданне новай фічы закранае мінімальную колькасць файлаў/класаў. У ідэале - адзін.

тлумачэнні

Бо адказнасць (за фічу ці баг) инкапсулированна ў адным файле/класе, тыя вы сапраўды ведаеце дзе шукаць і што кіраваць. Напрыклад: фіча змены высновы лагіроўкі аперацый запатрабуе змяніць толькі лагіроўшчык. Бегаць па ўсім астатнім кодзе не патрабуецца.

Іншы прыклад – даданне новага UI-кантролю, падобнага з папярэднімі. Калі гэта прымушае вас дадаць 10 розных сутнасцяў і 15 розных канвертараў - здаецца, вы "перадрабілі".

3) Калі некалькі распрацоўшчыкаў працуюць над рознымі фічамі вашага праекта, то верагоднасць мердж-канфлікту, гэта значыць верагоднасць таго, што адзін і той жа файл / клас будзе зменены ў некалькіх распрацоўшчыкаў адначасова – мінімальная.

тлумачэнні

Калі пры даданні новай аперацыі "Выліць гарэлку пад стол" вам трэба закрануць лагіроўшчык, аперацыю выпівання і вылівання - то падобна, што адказнасці падзелены крыва. Безумоўна, гэта не заўсёды магчыма, але трэба імкнуцца зменшыць гэты паказчык.

4) Пры ўдакладняючым пытанні пра бізнэс логіку (ад распрацоўніка ці мэнэджара) вы лезеце строга ў адзін клас/файл і атрымліваеце інфармацыю толькі ад туды.

тлумачэнні

Фічы, правілы або алгарытмы кампактна напісаны кожная ў адным месцы, а не раскіданы сцягамі па ўсёй прасторы кода.

5) Нэймінг зразумелы.

тлумачэнні

Наш клас ці метад адказны за нешта адно, і адказнасць адлюстравана ў яго назве

AllManagersManagerService – хутчэй за ўсё, God-клас
LocalPayment – ​​верагодна, не

Фармалізм 3. Методыка распрацоўкі "Оккама-first".

У пачатку праектавання, чалавек-малпачка не ведае і не адчувае ўсіх тонкасцяў развязальнай задачы і можа даць маху. Памыляцца можна па-рознаму:

  • Зрабіць занадта вялікія аб'екты, склеіўшы розныя адказнасці
  • Перадрабіць, падзяліўшы адзіную адказнасць на шмат розных тыпаў
  • Няправільна вызначыць межы адказнасці

Важна запомніць правіла: "памыляцца лепш у вялікі бок", ці "не ўпэўненыя - не дробніце". Калі, напрыклад, ваш клас збірае ў сабе дзве адказнасці - то ён па-ранейшаму зразумелы і яго можна распілаваць на два з мінімальнай зменай кліенцкага кода. Збіраць жа з аскепкаў шкла шклянку, як правіла, складаней з-за размазанага па некалькіх файлах кантэксту і адсутнасці неабходных залежнасцяў у кліенцкім кодзе.

Час закругляцца

Сфера прымянення SRP не абмяжоўваецца ААП і SOLID. Ён дастасоўны да метадаў, функцый, класаў, модуляў, мікрасэрвісаў і сэрвісаў. Ён прымяняецца як да "фігакс-фігакс-і-у-прод", так і да "рокет-сайнс" распрацоўцы, усюды робячы свет крыху лепш. Калі задумацца, тое гэта ці ледзь не фундаментальны прынцып усёй інжынерыі. Машынабудаванне, сістэмы кіравання, ды і наогул усе складаныя сістэмы – будуюцца з кампанентаў, і “недадрабненне” пазбаўляе канструктараў гнуткасці, “перадрабленне” – эфектыўнасці, а няслушныя межы – розуму і душэўнага спакою.

Single Responsibility Principle. Не такі просты, як здаецца

SRP не прыдуманы прыродай і не з'яўляецца часткай дакладнай навукі. Ён вылазіць з нашых з вамі біялагічных і псіхалагічных абмежаванняў. Гэта ўсяго толькі спосаб кантраляваць і развіваць складаныя сістэмы пры дапамозе мозгу чалавека-малпы. Ён расказвае нам, як дэкампазіраваць сістэму. Першапачатковая фармулёўка патрабавала ладнага навыку тэлепатыі, але спадзяюся, гэты артыкул злёгку развеяла дымавую заслону.

Крыніца: habr.com

Дадаць каментар