Principiul responsabilității unice. Nu atât de simplu pe cât pare

Principiul responsabilității unice. Nu atât de simplu pe cât pare Principiul responsabilității unice, cunoscut și ca principiul responsabilității unice,
alias principiul variabilității uniforme - un tip extrem de alunecos de înțeles și o întrebare atât de nervoasă la un interviu cu programatorul.

Prima cunoaștere serioasă cu acest principiu a avut loc la începutul primului an, când cei tineri și verzi au fost duși în pădure pentru a face elevi din larve – adevărați elevi.

În pădure, eram împărțiți în grupuri de câte 8-9 persoane și aveam o competiție - care grup bea cel mai repede o sticlă de votcă, cu condiția ca prima persoană din grup să toarne vodcă într-un pahar, a doua să o bea, iar al treilea are o gustare. Unitatea care și-a încheiat operațiunea se deplasează la sfârșitul cozii grupului.

Cazul în care dimensiunea cozii a fost un multiplu de trei a fost o implementare bună a SRP.

Definiție 1. Responsabilitate unică.

Definiția oficială a Principiului responsabilității unice (SRP) prevede că fiecare entitate are propria responsabilitate și motiv de existență și are o singură responsabilitate.

Luați în considerare obiectul „Drinker” (Tippler).
Pentru a implementa principiul SRP, vom împărți responsabilitățile în trei:

  • Se toarnă unul (Operațiunea de turnare)
  • Unul bea (DrinkUpOperation)
  • Unul are o gustare (TakeBiteOperation)

Fiecare dintre participanții la proces este responsabil pentru o componentă a procesului, adică are o responsabilitate atomică - de a bea, de a turna sau de a gusta.

La rândul său, groapa de băut este o fațadă pentru aceste operațiuni:

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

Principiul responsabilității unice. Nu atât de simplu pe cât pare

De ce?

Programatorul uman scrie cod pentru omul-maimuță, iar omul-maimuță este neatent, prost și mereu grăbit. El poate ține și înțelege aproximativ 3 - 7 termeni la un moment dat.
În cazul unui bețiv, există trei dintre acești termeni. Totuși, dacă scriem codul cu o singură coală, atunci acesta va conține mâini, ochelari, lupte și nenumărate dispute despre politică. Și toate acestea vor fi în corpul unei singure metode. Sunt sigur că ați văzut un astfel de cod în practica dumneavoastră. Nu este cel mai uman test pentru psihic.

Pe de altă parte, omul maimuță este proiectat să simuleze obiecte din lumea reală în capul său. În imaginația lui, le poate împinge împreună, asambla noi obiecte din ele și le poate dezasambla în același mod. Imaginează-ți un model vechi de mașină. În imaginația ta, poți deschide ușa, deșuruba garnitura ușii și vezi acolo mecanismele de ridicare a geamurilor, în interiorul cărora vor fi angrenaje. Dar nu poți vedea toate componentele mașinii în același timp, într-o singură „listare”. Cel puțin „omul maimuță” nu poate.

Prin urmare, programatorii umani descompun mecanismele complexe într-un set de elemente mai puțin complexe și funcționale. Cu toate acestea, poate fi descompus în diferite moduri: în multe mașini vechi, conducta de aer intră în ușă, iar la mașinile moderne, o defecțiune a electronicii de blocare împiedică pornirea motorului, ceea ce poate fi o problemă în timpul reparațiilor.

acum, SRP este un principiu care explică CUM se descompune, adică unde se trasează linia de despărțire.

El spune că este necesar să se descompună după principiul împărțirii „responsabilității”, adică în funcție de sarcinile anumitor obiecte.

Principiul responsabilității unice. Nu atât de simplu pe cât pare

Să ne întoarcem la băutură și la avantajele pe care omul maimuță le primește în timpul descompunerii:

  • Codul a devenit extrem de clar la fiecare nivel
  • Codul poate fi scris de mai mulți programatori simultan (fiecare scrie un element separat)
  • Testarea automată este simplificată - cu cât elementul este mai simplu, cu atât este mai ușor de testat
  • Apare compoziționalitatea codului - puteți înlocui DrinkUpOperation la o operaţie în care un beţiv toarnă lichid sub masă. Sau înlocuiți operația de turnare cu o operațiune în care amestecați vin și apă sau vodcă și bere. În funcție de cerințele afacerii, puteți face totul fără să atingeți codul metodei Tippler.Act.
  • Din aceste operațiuni puteți plia lacomul (folosind doar TakeBitOperation), Alcool (numai consumând DrinkUpOperation direct din sticlă) și îndeplinesc multe alte cerințe de afaceri.

(Oh, se pare că acesta este deja un principiu OCP și am încălcat responsabilitatea acestei postări)

Și, desigur, contra:

  • Va trebui să creăm mai multe tipuri.
  • Un bețiv bea pentru prima dată cu câteva ore mai târziu decât ar fi făcut-o altfel.

Definiția 2. Variabilitatea unificată.

Permiteți-mi, domnilor! Cursul de băutură are și o singură responsabilitate - bea! Și, în general, cuvântul „responsabilitate” este un concept extrem de vag. Cineva este responsabil pentru soarta umanității, iar cineva este responsabil pentru creșterea pinguinilor care au fost răsturnați la stâlp.

Să luăm în considerare două implementări ale băutorului. Prima, menționată mai sus, conține trei clase - turnare, băutură și gustare.

Al doilea este scris prin metodologia „Forward and Only Forward” și conține toată logica din 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);
    }
   }
}

Ambele clase, din punctul de vedere al unui observator din afară, arată exact la fel și au aceeași responsabilitate de „băutură”.

Confuzie!

Apoi intrăm online și aflăm o altă definiție a SRP - Principiul unic al schimbării.

SCP afirmă că „Un modul are un singur motiv pentru a se schimba". Adică, „Responsabilitatea este un motiv pentru schimbare”.

(Se pare că băieții care au venit cu definiția originală erau încrezători în abilitățile telepatice ale omului maimuță)

Acum totul cade la locul lui. Separat, putem schimba procedurile de turnare, băutură și gustare, dar în băutor în sine putem schimba numai succesiunea și compoziția operațiilor, de exemplu, prin mutarea gustarii înainte de a bea sau adăugând citirea unui toast.

În abordarea „Înainte și numai înainte”, tot ceea ce poate fi schimbat este schimbat doar în metodă act. Acest lucru poate fi lizibil și eficient atunci când există puțină logică și se schimbă rar, dar de multe ori ajunge în metode groaznice de 500 de rânduri fiecare, cu mai multe declarații if decât este necesar pentru aderarea Rusiei la NATO.

Definiție 3. Localizarea modificărilor.

Băutorii de multe ori nu înțeleg de ce s-au trezit în apartamentul altcuiva sau unde este telefonul lor mobil. Este timpul să adăugați înregistrări detaliate.

Să începem înregistrarea cu procesul de turnare:

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

Prin încapsularea lui în Operațiunea de turnare, am actionat cu intelepciune din punct de vedere al responsabilitatii si al incapsularii, dar acum suntem confundati cu principiul variabilitatii. Pe lângă operațiunea în sine, care se poate modifica, înregistrarea în sine devine și ea modificabilă. Va trebui să separați și să creați un jurnal special pentru operația de turnare:

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

Cititorul meticulos va observa asta LogAfter, LogInainte и OnError poate fi schimbat și individual și, prin analogie cu pașii anteriori, se vor crea trei clase: PourLoggerBefore, PourLoggerAfter и PourErrorLogger.

Și amintindu-ne că există trei operațiuni pentru un băutor, primim nouă clase de exploatare forestieră. Ca urmare, întregul cerc de băutură este format din 14 (!!!) clase.

Hiperbolă? Cu greu! Un om maimuță cu o grenadă de descompunere va împărți „turnătorul” într-un decantor, un pahar, operatori de turnare, un serviciu de alimentare cu apă, un model fizic al ciocnirii moleculelor, iar pentru trimestrul următor va încerca să dezlege dependențele fără variabile globale. Și crede-mă, nu se va opri.

În acest moment, mulți ajung la concluzia că SRP sunt basme din regate roz și pleacă să joace tăiței...

... fără a afla vreodată despre existența unei a treia definiții a Srp:

„Principiul responsabilității unice prevede că lucrurile care sunt similare cu schimbarea ar trebui să fie stocate într-un singur loc". sau "Ceea ce se schimbă împreună ar trebui păstrat într-un singur locMatei 22:21

Adică, dacă schimbăm înregistrarea unei operațiuni, atunci trebuie să o schimbăm într-un singur loc.

Acesta este un punct foarte important - deoarece toate explicațiile SRP de mai sus spuneau că este necesar să se zdrobească tipurile în timp ce acestea erau zdrobite, adică au impus o „limită superioară” a dimensiunii obiectului, iar acum vorbim deja despre o „limită inferioară” . Cu alte cuvinte, SRP nu necesită doar „zdrobire în timp ce se zdrobește”, dar și să nu exagereze - „nu zdrobi lucrurile care se interconectează”. Aceasta este marea bătălie dintre briciul lui Occam și omul maimuță!

Principiul responsabilității unice. Nu atât de simplu pe cât pare

Acum băutorul ar trebui să se simtă mai bine. Pe lângă faptul că nu este nevoie să împărțim loggerul IPourLogger în trei clase, putem, de asemenea, să combinam toate loggerul într-un singur tip:

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

Și dacă adăugăm un al patrulea tip de operație, atunci înregistrarea pentru acesta este deja gata. Și codul operațiunilor în sine este curat și lipsit de zgomot de infrastructură.

Ca urmare, avem 5 clase pentru rezolvarea problemei băuturii:

  • Operație de turnare
  • Operațiune de băut
  • Operație de bruiaj
  • Logger
  • Fațada băutorului

Fiecare dintre ele este responsabil strict pentru o singură funcționalitate și are un motiv pentru schimbare. Toate regulile similare cu schimbarea se află în apropiere.

Exemplu din viața reală

Am scris odată un serviciu pentru înregistrarea automată a unui client b2b. Și a apărut o metodă DUMNEZEU pentru 200 de rânduri cu conținut similar:

  • Accesați 1C și creați un cont
  • Cu acest cont, accesați modulul de plată și creați-l acolo
  • Verificați dacă un cont cu un astfel de cont nu a fost creat pe serverul principal
  • Creați un cont nou
  • Adăugați rezultatele înregistrării în modulul de plată și numărul 1c la serviciul de rezultate de înregistrare
  • Adăugați informații despre cont în acest tabel
  • Creați un număr de punct pentru acest client în serviciul de puncte. Transmiteți numărul de cont 1c acestui serviciu.

Și mai erau aproximativ 10 operațiuni de afaceri pe această listă cu conectivitate teribilă. Aproape toată lumea avea nevoie de obiectul contului. ID-ul punctului și numele clientului au fost necesare în jumătate din apeluri.

După o oră de refactorizare, am reușit să separăm codul de infrastructură și unele dintre nuanțele de lucru cu un cont în metode/clase separate. Metoda lui Dumnezeu a făcut totul mai ușor, dar au mai rămas 100 de linii de cod care pur și simplu nu doreau să fie descurcate.

Abia după câteva zile a devenit clar că esența acestei metode „ușoare” este un algoritm de afaceri. Și că descrierea inițială a specificațiilor tehnice era destul de complexă. Și este încercarea de a sparge această metodă în bucăți care va încălca SRP-ul, și nu invers.

Formalism.

E timpul să ne lăsăm bețivul în pace. Uscați-vă lacrimile - cu siguranță vom reveni la el cândva. Acum să oficializăm cunoștințele din acest articol.

Formalism 1. Definiția SRP

  1. Separați elementele astfel încât fiecare dintre ele să fie responsabil pentru un singur lucru.
  2. Responsabilitatea înseamnă „motivul de a schimba”. Adică, fiecare element are un singur motiv de schimbare, în ceea ce privește logica de business.
  3. Modificări potențiale ale logicii de afaceri. trebuie localizat. Elementele care se schimbă sincron trebuie să fie în apropiere.

Formalism 2. Criterii de autotestare necesare.

Nu am văzut suficiente criterii pentru îndeplinirea SRP. Dar există condiții necesare:

1) Întrebați-vă ce face această clasă/metodă/modul/serviciu. trebuie să-i răspunzi cu o definiție simplă. ( Mulțumesc Brightori )

explicatii

Cu toate acestea, uneori este foarte dificil să găsești o definiție simplă

2) Remedierea unei erori sau adăugarea unei noi caracteristici afectează un număr minim de fișiere/clase. Ideal - unul.

explicatii

Deoarece responsabilitatea (pentru o caracteristică sau o eroare) este încapsulată într-un singur fișier/clasă, știți exact unde să căutați și ce să editați. De exemplu: caracteristica de modificare a ieșirii operațiunilor de înregistrare va necesita schimbarea doar a loggerului. Nu este nevoie să parcurgeți restul codului.

Un alt exemplu este adăugarea unui nou control UI, similar cu cele anterioare. Dacă acest lucru te obligă să adaugi 10 entități diferite și 15 convertoare diferite, se pare că exagerezi.

3) Dacă mai mulți dezvoltatori lucrează la diferite caracteristici ale proiectului dvs., atunci probabilitatea unui conflict de îmbinare, adică probabilitatea ca același fișier/clasă să fie schimbat de mai mulți dezvoltatori în același timp, este minimă.

explicatii

Dacă, atunci când adăugați o nouă operațiune „Toarnă vodcă sub masă”, trebuie să afectați loggerul, operațiunea de băut și turnare, atunci se pare că responsabilitățile sunt împărțite strâmb. Desigur, acest lucru nu este întotdeauna posibil, dar ar trebui să încercăm să reducem această cifră.

4) Când vi se pune o întrebare clarificatoare despre logica afacerii (de la un dezvoltator sau manager), intrați strict într-o clasă/fișier și primiți informații doar de acolo.

explicatii

Caracteristicile, regulile sau algoritmii sunt scrise compact, fiecare într-un singur loc și nu sunt împrăștiate cu steaguri în spațiul de cod.

5) Denumirea este clară.

explicatii

Clasa sau metoda noastră este responsabilă pentru un lucru, iar responsabilitatea se reflectă în numele ei

AllManagersManagerService - cel mai probabil o clasă Dumnezeu
LocalPayment - probabil că nu

Formalism 3. Metodologia de dezvoltare Occam-first.

La începutul designului, omul maimuță nu știe și nu simte toate subtilitățile problemei care se rezolvă și poate greși. Puteți face greșeli în diferite moduri:

  • Faceți obiectele prea mari prin îmbinarea diferitelor responsabilități
  • Reîncadrarea prin împărțirea unei singure responsabilități în mai multe tipuri diferite
  • Definiți incorect limitele responsabilității

Este important să rețineți regula: „este mai bine să faci o mare greșeală” sau „dacă nu ești sigur, nu o împărți”. Dacă, de exemplu, clasa dvs. conține două responsabilități, atunci este încă de înțeles și poate fi împărțită în două cu modificări minime la codul clientului. Asamblarea unui pahar din cioburi de sticlă este de obicei mai dificilă din cauza contextului răspândit în mai multe fișiere și a lipsei dependențelor necesare în codul clientului.

Este timpul să numim o zi

Sfera de aplicare a SRP nu se limitează la OOP și SOLID. Se aplică metodelor, funcțiilor, claselor, modulelor, microserviciilor și serviciilor. Se aplică atât dezvoltării „figax-figax-and-prod”, cât și „rocket-science”, făcând lumea puțin mai bună peste tot. Dacă vă gândiți bine, acesta este aproape principiul fundamental al tuturor ingineriei. Ingineria mecanică, sistemele de control și, într-adevăr, toate sistemele complexe sunt construite din componente, iar „subfragmentarea” îi privează pe proiectanți de flexibilitate, „suprafragmentarea” îi privează pe proiectanți de eficiență, iar limitele incorecte îi privează de rațiune și liniște sufletească.

Principiul responsabilității unice. Nu atât de simplu pe cât pare

SRP nu este inventat de natură și nu face parte din știința exactă. Ea iese din limitările noastre biologice și psihologice.Este doar o modalitate de a controla și dezvolta sisteme complexe folosind creierul omului-maimuță. El ne spune cum să descompunem un sistem. Formularea originală a necesitat o cantitate destul de mare de telepatie, dar sper că acest articol înlătură o parte din cortina de fum.

Sursa: www.habr.com

Adauga un comentariu