Princip jedné odpovědnosti. Není tak jednoduchý, jak se zdá

Princip jedné odpovědnosti. Není tak jednoduchý, jak se zdá Princip jedné odpovědnosti, také známý jako princip jediné odpovědnosti,
také nazývaný princip jediné změny – je velmi kluzký pro pochopení a nervózní téma na pohovoru pro programátora.

Moje první vážné seznámení s tímto principem se uskutečnilo na začátku prvního ročníku, kdy nás mladé a nezkušené vzali do lesa, abychom se z larev studentů stali skutečnými studenty.

V lese nás rozdělili do skupin po 8-9 lidech a uspořádali soutěž – která skupina rychleji vypije láhev vodky, přičemž první člověk ze skupiny nalévá vodku do sklenice, druhý vypije a třetí zakousne. Účastník, který dokončí svou úlohu, se postaví na konec fronty skupiny.

Případ, kdy je počet ve frontě dělitelný třemi, byl dobrou implementací SRP.

Definice 1. Jediná odpovědnost.

Oficiální definice principu jediné odpovědnosti (SRP) říká, že každý objekt má svou odpovědnost a důvod existence a že tato odpovědnost je pouze jedna.

Zvažme objekt „Vypijač“ (Tippler).
Abychom dodrželi princip SRP, rozdělíme úkoly na tři:

  • Jeden nalévá (PourOperation)
  • Jeden vypije (DrinkUpOperation)
  • Jeden zakousne (TakeBiteOperation)

Každý z účastníků procesu odpovídá za jednu složku procesu, tj. má jednu atomární odpovědnost – vypít, nalít nebo zakousnout.

Vypijač je zase fasádou pro tato data:

class Tippler {
    //...
    void Act(){
        _pourOperation.Do() // nalít
        _drinkUpOperation.Do() // vypít
        _takeBiteOperation.Do() // zakousnout
    }
}

Princip jedné odpovědnosti. Není tak jednoduchý, jak se zdá

Proč?

Člověk-programátor píše kód pro člověka-opici, a člověk-opice je nepozorný, hloupý a neustále někam spěchá. Může udržet a pochopit asi 3 až 7 termínů najednou.
V případě vypijače jsou tyto termíny tři. Pokud ale napíšeme kód jako jedinou plachtu, objeví se v něm ruce, sklenice, rvačky a nekonečné spory o politice. A to všechno bude v těle jedné metody. Jsem si jistý, že jste takový kód viděli ve své praxi. Není to nejhumánnější zkouška pro psychiku.

Na druhou stranu je člověk-opice zaměřen na modelování objektů reálného světa ve své hlavě. Ve své fantazii je může srážet, skládat z nich nové objekty a rovněž je rozebírat. Představte si starý model auta. Můžete si v mysli otevřít dveře, odšroubovat čalounění dveří a vidět tam mechanismy okna, uvnitř kterých budou převody. Ale nemůžete vidět všechny komponenty auta najednou, v jednom "výpisu". Tedy alespoň "člověk-opice" to nedokáže.

Proto programátoři-dekompozitoři rozkládají složité mechanismy na součásti méně složité a funkční. Nicméně dekompozice může být různá: ve spoustě starých aut vede vzduchovod do dveří, zatímco v moderních vozech selhání elektroniky zámku brání nastartování motoru, což způsobuje problémy při opravě.

A tak je to, SRP — to je princip, který vysvětluje JAK dekomponovat, tedy kde udělat čáru rozdělení..

Říká, že dekomponovat je třeba podle principu rozdělení "odpovědnosti", tedy podle úkolů různých objektů.

Princip jedné odpovědnosti. Není tak jednoduchý, jak se zdá

Vraťme se k opilci a výhodám, které získává člověk-opice při dekompozici:

  • Kód se stal naprosto jasným na každé úrovni.
  • Kód mohou psát najednou tři různí programátoři (každý píše samostatný prvek).
  • Ulehčuje automatizované testování — čím jednodušší je prvek, tím snazší je jeho testování.
  • Objevuje se kompozicionalita kódu — můžete vyměnit. DrinkUpOperation za operaci, při které opilec vylévá tekutinu pod stůl. Nebo vyměnit operaci nalévání za operaci, při které mícháte víno a vodu nebo vodku a pivo. V závislosti na požadavcích firmy můžete dělat cokoliv, přičemž se nedotýkáte kódu metody. Tippler.Act.
  • Z těchto operací můžete složit obžeru (pouze pomocí. TakeBitOperation), Alkoholika (pouze pomocí. DrinkUpOperation přímo z láhve) a uspokojit mnoho dalších podnikových požadavků.

(Och, zdá se, že to už je princip OCP, a porušil jsem odpovědnost tohoto příspěvku.)

A samozřejmě také nevýhody:

  • Bude nutné vytvořit více typů.
  • Opilec poprvé vypije o pár hodin později, než by mohl.

Definice 2. Jednotná variabilita.

Dovolte, pánové! Třída opilce má také jedinou odpovědnost – pít! A vůbec, pojem 'odpovědnost' je velmi vágní. Někdo je odpovědný za osud lidstva, jiný za zvedání na zemi ležících tučňáků na pólu.

Podívejme se na dvě realizace opilce. První, zmíněná výše, obsahuje tři třídy – nalít, vypít a zakousnout.

Druhá je napsaná podle metodologie 'Kupředu a jen kupředu' a obsahuje veškerou logiku v metodě 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);
    }
   }
}

Obě tyto třídy, z pohledu externího pozorovatele, vypadají naprosto identicky a mají jedinou odpovědnost 'vypít'.

Trapnost!

Pak se vrhneme na internet a zjistíme jiné definice SRP – Princip jediné měnitelnosti (Single Changeability Principle).

SCP říká, že 'Modul má jeden a pouze jeden důvod ke změně'. Tedy 'Odpovědnost je důvodem ke změně'.

(Zdá se, že kluci, kteří vymysleli původní definici, byli přesvědčeni o telepatických schopnostech člověka-opice.)

Teď už vše začíná dávat smysl. Samostatně můžeme měnit procesy nalévání, pití a zakousnutí, a v samotném opilci můžeme měnit jen pořadí a složení operací, například přesunutím zakousnutí před pitím nebo přidáním četby přípitku.

V přístupu 'Kupředu a jen kupředu' se vše, co lze změnit – mění pouze v metodě Act. To může být čitelné a efektivní, když je logika malá a mění se zřídka, ale často to končí hroznými metodami po 500 řádcích v každém, s počtem if-ů větším, než je potřeba pro vstup Ruska do NATO.

Definice 3. Lokalizace změn.

Opilci často nechápou, proč se probudili v cizím bytě, nebo kde mají svůj mobil. Přišel čas přidat podrobnou logování.

Začneme logování procesem nalévání:

class PourOperation: IOperation{
    PourOperation(ILogger log /*....*/){/*...*/}
    //...
    void Do(){
        _log.Log($"Před nalitím s {_hand} a {_bottle}");
        //Piti obchodní logika ...
        _log.Log($"Po nalití s {_hand} a {_bottle}");
    }
}

Zabalíme ji do PourOperation, postupovali jsme moudře z hlediska odpovědnosti a enkapsulace, ale máme nyní problém s principem proměnlivosti. Vedle samotné operace, která se může měnit, se stává proměnlivým také samotné logování. Budeme muset rozdělit a vytvořit speciální logovač pro operaci nalévání:

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);
        }
    }
}

Pečlivý čtenář si všimne, že LogAfter, LogBefore a OnError také se mohou měnit samostatně, a analogicky k předchozím akcím vytvoří tři třídy: PourLoggerBefore, PourLoggerAfter a PourErrorLogger.

A když si vzpomeneme, že operací pro nalévání je tři – dostaneme devět tříd logování. Nakonec celý systém nalévání se skládá ze 14 (!!!) tříd.

Hyperbola? Sotva! Člověk-opice s dekompoziční granátou rozbije „nalévače“ na karafu, sklenici, operátory nalévání, servis dodávky vody, fyzikální model srážky molekul a v příštím čtvrtletí se bude snažit rozplést závislosti bez globálních proměnných. A věřte, že se nezastaví.

Právě v tomto okamžiku mnoho lidí dospěje k závěru, že SRP je pohádka z růžových království, a odchází zpět k rozplétání…

… aniž by se dozvěděli o existenci třetího vymezení Srp:

„Princip jediné odpovědnosti říká, že podobné věci, které potřebují změnu, by měly být uchovávány na jednom místě“. nebo “To, co se mění společně, by mělo být uchováváno na jednom místě.”

To znamená, že pokud měníme logování operace, měli bychom to měnit na jednom místě.

To je velmi důležitý okamžik – protože všechna předchozí vysvětlení SRP hovořila o tom, že je třeba dělit typy, dokud se dají dělit, což vytváří „horní omezení“ na velikost objektu, a nyní hovoříme také o „dolním omezení“. Jinými slovy, SRP nejen vyžaduje „dělit, dokud se dá dělit“, ale také se nevyhnout – „nepřehnat to s dělením propojených věcí“. To je velký souboj Ockhamovy břitvy s člověkem-opicí!

Princip jedné odpovědnosti. Není tak jednoduchý, jak se zdá

Nyní by mělo být pro nalévače snazší. Kromě toho, že není nutné rozdělovat logovač IPourLogger na tři třídy, můžeme také spojit všechny logovače do jednoho typu:

class OperationLogger{
    public OperationLogger(string operationName){
/*..*/}
    public void LogBefore(object[] args){
/*...*/}
    public void LogAfter(object[] args){
/*..*/}
    public void LogError(object[] args, exception e){
/*..*/}
}

A pokud se nám přidá čtvrtý typ operace, je pro něj již připraveno logování. A kód samotných operací je čistý a zbavený infrastrukturního šumu.

V důsledku máme 5 tříd pro řešení úkolu vylévání:

  • Operace nalévání
  • Operace vylévání
  • Operace zakusování
  • Logovač
  • Fasáda pijana

Každý z nich odpovídá striktně za jednu funkčnost, má jeden důvod pro změnu. Všechna podobná pravidla pro změny leží blízko sebe.

Příklad z reálného života

Jednou jsme psali službu automatické registrace b2b klienta. A objevil se GOD -metoda o 200 řádcích podobného obsahu:

  • Jdi do 1C a založ účet
  • S tímto účtem jdi k platebnímu modulu a založ ho tam
  • Zkontroluj, že účet s tímto číslem není vytvořen v hlavním serveru
  • Vytvoř nový účet
  • Výsledek registrace v platebním modulu a číslo 1c přidej do služby výsledků registrace
  • Přidej do této tabulky informace o účtu
  • Vytvoř číslo bodu pro tohoto klienta v servisním bodě. Předej do tohoto servisu číslo účtu 1c.

A bylo v tomto seznamu ještě asi 10 obchodních operací s děsivou provázaností. Objekt účtu potřeboval téměř každý. Identifikátor bodu a jméno klienta byly potřeba v polovině volání.

Po hodinové refaktorizaci jsme dokázali oddělit infrastrukturní kód a některé nuance práce s účtem do samostatných metod/tříd. God metoda zhubla, ale zůstalo 100 řádků kódu, které se nechtěly rozplést.

Až po několika dnech přišlo pochopení, že podstatou této "zhublé" metody je právě obchodní algoritmus. A že původní popis TŠ byl docela složitý. A právě pokus o rozbití této metody do kusů by porušil SRP, a ne naopak.

Formalismus.

Přišel čas nechat našeho pijana na pokoji. Utřete slzy — určitě se k němu někdy vrátíme. A teď formalizujeme znalosti z tohoto článku.

Formalismus 1. Definice SRP

  1. Rozdělujte prvky tak, aby každý z nich byl odpovědný za něco jednoho.
  2. Odpovědnost se rozkládá jako „důvod ke změně“. To znamená, že každý prvek má pouze jeden důvod pro změnu, pokud jde o obchodní logiku.
  3. Potenciální změny obchodní logiky by měly být lokalizovány. Změnitelné synchronní prvky by měly být vedle sebe.

Formalismus 2. Nezbytná kritéria sebehodnocení.

Nesetkal jsem se s dostatečnými kritérii splnění SRP. Existují však nezbytné podmínky:

1) Zeptejte se sami sebe — co dělá tato třída/metoda/modul/služba? Na tuto otázku byste měli odpovědět jednoduchou definicí. (Děkuji) Brightori )

vysvětlení

Nicméně, někdy je velmi obtížné najít jednoduchou definici.

2) Oprava nějaké chyby nebo přidání nové funkcionality zasahuje do minimálního počtu souborů/tříd. Ideálně — pouze jedna.

vysvětlení

Jelikož je odpovědnost (za funkci nebo chybu) enkapsulována v jednom souboru/třídě, přesně víte, kde hledat a co opravovat. Například: funkce změny výstupu logování operací vyžaduje změnu pouze v loggeru. Není potřeba skákat po celém zbytku kódu.

Další příklad — přidání nové UI kontroly, která je podobná těm předchozím. Pokud vás to nutí přidat 10 různých entit a 15 různých konvertorů — zdá se, že jste "přehnali".

3) Pokud několik vývojářů pracuje na různých funkcionalitách vašeho projektu, pravděpodobnost konfliktu slučování, tj. pravděpodobnost, že stejný soubor/třída bude změněna několika vývojáři současně, je minimální.

vysvětlení

Pokud při přidání nové operace "Vylít vodku pod stůl" musíte zasáhnout do loggeru, operace pití a vylévání — pak to vypadá, že odpovědnosti jsou rozděleny nesprávně. Samozřejmě, to není vždy možné, ale měli byste se snažit tento ukazatel snížit.

4) Při upřesňující otázce k obchodní logice (od vývojáře nebo manažera) se dostáváte přímo do jedné třídy/souboru a získáváte informace pouze odsud.

vysvětlení

Funkce, pravidla nebo algoritmy jsou kompaktně zapsány, každá na jednom místě, a ne roztroušeny vlajkami po celém kódu.

5) Pojmenování je srozumitelné.

vysvětlení

Naše třída nebo metoda je zodpovědná za něco konkrétního, a tato zodpovědnost je odrazena v jejím názvu.

AllManagersManagerService — pravděpodobně God-třída.
LocalPayment — pravděpodobně ne.

Formalismus 3. Metodika vývoje „Ockham-first“.

Na začátku navrhování, člověk-opice nezná a necítí všechny nuance řešeného úkolu a může udělat chybu. Chybovat lze různými způsoby:

  • Vytvořit příliš velké objekty sloučením různých odpovědností
  • Rozmělnit, rozdělením jediné odpovědnosti na mnoho různých typů
  • Nesprávně stanovit hranice odpovědnosti

Je důležité si zapamatovat pravidlo: „lépe se mýlit ve velkém“, nebo „nejste si jisti – nedělejte to“. Pokud například váš třída sbírá dvě odpovědnosti – zůstává jasná a můžete ji rozdělit na dvě s minimálními změnami v klientském kódu. Sestavit sklenici z kousků skla je obvykle obtížnější kvůli rozptýlenému kontextu v několika souborech a nedostatku potřebných závislostí v klientském kódu.

Je čas to uzavřít

Oblast použití SRP není omezena na OOP a SOLID. Je aplikovatelný na metody, funkce, třídy, moduly, mikroslužby a služby. Je použitelný jak pro „figaks-figaks-a-do-produkce“, tak pro „raketové vědy“ ve vývoji, všude činí svět o něco lepším. Když nad tím uvažujete, je to snad až fundamentální princip celé inženýrství. Strojírenství, systémy řízení, a vůbec všechny složité systémy – jsou budovány z komponentů, a „nedělení“ zbavuje konstruktéry flexibility, „předešlé dělení“ – efektivity, a nesprávné hranice – rozumu a duševního klidu.

Princip jedné odpovědnosti. Není tak jednoduchý, jak se zdá

SRP není výmysl přírody a není součástí exaktní vědy. Vzniká z našich biologických a psychologických omezení. Je to jen způsob, jak řídit a rozvíjet složité systémy pomocí lidského mozku. Říká nám, jak dekomponovat systém. Původní formulace vyžadovala značnou dovednost telepatie, ale doufám, že tento článek trochu rozptýlil mlhu.

Zdroj: habr.com

Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster