Principiul responsabilității unice. Nu este atât de simplu cum pare

Principiul responsabilității unice. Nu este atât de simplu cum pare Principiul responsabilității unice, adică principiul responsabilității unice,
adică principiul modificabilității unice - este un concept extrem de greu de înțeles și o întrebare foarte delicată în interviurile pentru programatori.

Prima mea întâlnire serioasă cu acest principiu a avut loc la începutul primului an de studii, când tinerii și neexperimentați, ne-au dus în pădure pentru a ne transforma din larve de studenți în studenți adevărați.

În pădure, am fost împărțiți în grupuri de 8-9 persoane fiecare și am organizat o competiție - care grup va bea cel mai repede o sticlă de votcă, cu condiția ca prima persoană din grup să toarne votca în pahar, a doua să bea, iar a treia să mânânce ceva. Persoana care și-a finalizat atribuția devine ultima din coada grupului.

Cazul în care dimensiunea cozii era divizibilă cu trei a reprezentat o bună implementare a SRP.

Definiția 1. Responsabilitate unică.

Definiția oficială a principiului responsabilității unice (SRP) afirmă că fiecare obiect are o responsabilitate și un motiv de existență, iar această responsabilitate este una singură.

Să luăm în considerare obiectul „Bețiv” (Tippler).
Pentru a respecta principiul SRP, vom împărți responsabilitățile în trei:

  • Unul toarnă (PourOperation)
  • Unul bea (DrinkUpOperation)
  • Unul mănâncă (TakeBiteOperation)

Fiecare participant la proces este responsabil pentru o componentă a procesului, adică are o responsabilitate atomică - a bea, a turna sau a mânca.

Bețivul, la rândul său, este un fațadă pentru aceste operațiuni:

class Tippler {
    //...
    void Act(){
        _pourOperation.Do() // a turna
        _drinkUpOperation.Do() // a bea
        _takeBiteOperation.Do() // a mânca
    }
}

Principiul responsabilității unice. Nu este atât de simplu cum pare

De ce?

Programatorul scrie cod pentru o maimuță umană, iar maimuța umană este neatență, prostă și mereu pe fugă. Ea poate reține și înțelege aproximativ 3 - 7 termeni într-un moment dat.
În cazul bețivului, acești termeni sunt trei. Totuși, dacă vom scrie cod pe o singură foaie, vor apărea mâini, pahare, bătăi și dispute infinite despre politică. Și toate acestea vor fi în corpul unei metode. Sunt sigur că ați văzut astfel de cod în practica voastră. Nu este cel mai uman test pentru psihic.

Pe de altă parte, omul-maimuță este antrenat să modeleze obiecte din lumea reală în mintea sa. În imaginația sa, el poate să le ciocnească, să le asambleze în noi obiecte și, de asemenea, să le descompună. Imaginează-ți un model vechi de mașină. Poți închipui că deschizi o ușă, deșurubezi căptușeala ușii și vezi acolo mecanismele geamurilor electrice, în interiorul cărora se află roți dințate. Dar nu poți vedea toate componentele mașinii simultan, într-o „listare”. Cel puțin, „omul-maimuță” nu poate.

Din acest motiv, oamenii-programatori descompun mecanismele complexe în seturi de elemente mai puțin complexe și funcționale. Totuși, descompunerea se poate face în moduri diferite: în multe mașini vechi, conducta de aer iese în ușă, iar în cele moderne, o defecțiune a electronicii în blocaj nu permite pornirea motorului, ceea ce complica reparația.

Așadar, SRP - este un principiu care explică CUM să descompui, adică unde să trasezi linia de separare..

El spune că descompunerea trebuie să se facă după principiul separării „responsabilităților”, adică pe baza sarcinilor anumitor obiecte.

Principiul responsabilității unice. Nu este atât de simplu cum pare

Să revenim la bețiv și la avantajele pe care le obține omul-maimuță prin descompunere:

  • Codul a devenit extrem de clar la fiecare nivel.
  • Codul poate fi scris de mai mulți programatori simultan (fiecare scriind un element separat).
  • Se simplifică testarea automată - cu cât elementul este mai simplu, cu atât este mai ușor de testat.
  • Apără compoziția codului - poți înlocui DrinkUpOperation cu o operațiune în care bețivul vărsă lichid sub masă. Sau poți înlocui operațiunea de turnare cu o operațiune în care amesteci vin și apă sau vodcă și bere. În funcție de cerințele afacerii, poți face orice, fără a atinge codul metodei. Tippler.Act.
  • Din aceste operațiuni poți crea un mâncător (folosind doar TakeBitOperation), un alcoolic (folosind doar DrinkUpOperation direct din sticlă) și să satisface multe alte cerințe ale afacerii.

(Oi, pare că acesta este deja principiul OCP și am încălcat responsabilitatea acestui post)

Și, bineînțeles, dezavantajele:

  • Va trebui să creez mai multe tipuri.
  • Bețivul va bea pentru prima dată cu câteva ore mai târziu decât ar putea.

Definiția 2. Variabilitate unică.

Permiteți-mi, domnilor! Clasa „bețiv” are și ea o singură responsabilitate – aceea de a bea! Și, în general, cuvântul „responsabilitate” este un concept extrem de vag. Unii sunt responsabili pentru soarta umanității, iar alții sunt responsabili pentru a ridica pingvinii răsturnați pe pol.

Să examinăm două implementări ale bețivului. Prima, menționată mai sus, conține trei clase – a turna, a bea și a prânzi.

A doua, scrisă prin metodologia „Înaintare și doar înainte” și conține toată logica în metoda 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 aceste clase, din perspectiva unui spectator extern, par absolut identice și îndeplinesc o singură responsabilitate „a bea”.

Confuzie!

Atunci ne ducem pe internet și aflăm o altă definiție a SRP – Principiul unei singure variabile (Single Changeability Principle).

SCP afirmă că „Un modul are un singur și unic motiv pentru schimbare“. Deci, „Responsabilitatea este motivul schimbării”.

(Se pare că băieții care au creat definiția inițială erau convinși de abilitățile telepatice ale omului-maimuță)

Acum totul se așează la locul lui. Putem schimba separat procesele de turnare, băut și prânz, iar în bețiv putem schimba doar secvența și compunerea operațiunilor, de exemplu, mutând prânzul înainte de a bea sau adăugând citirea unui toast.

În abordarea „Înaintare și doar înainte”, tot ceea ce poate fi schimbat – se schimbă doar în metoda Act. Acest lucru poate fi lizibil și eficient în cazul în care logica este puțină și se schimbă rar, dar de multe ori se termină cu metode groaznice de 500 de linii fiecare, cu un număr de if-uri mai mare decât este necesar pentru aderarea Rusiei la NATO.

Definiția 3. Localizarea schimbărilor.

Bețivii adesea nu înțeleg de ce s-au trezit în apartamentul altcuiva, sau unde le este telefonul mobil. A sosit momentul să adăugăm un jurnal detaliat.

Să începem jurnalizarea cu procesul de turnare:

class PourOperation: IOperation{
    PourOperation(ILogger log /*....*/){/*...*/}
    //...
    void Do(){
        _log.Log($"Înainte de turnare cu {_hand} și {_bottle}");
        //Logică de afaceri pentru turnare ...
        _log.Log($"După turnare cu {_hand} și {_bottle}");
    }
}

Incepșându-l în PourOperation, am acționat în mod înțelept din punct de vedere al responsabilității și încapsulării, dar acum avem o confuzie cu privire la principiul variabilității. În afară de operația în sine, care poate varia, se schimbă și logarea. Va trebui să separăm și să creăm un logger 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{
             //... logica de afaceri
             _log.LogAfter(_hand, _bottle);
        }
        catch(exception e){
            _log.OnError(_hand, _bottle, e);
        }
    }
}

Cine este atent va observa că LogAfter, LogBefore și OnError de asemenea, pot varia separat, iar prin analogie cu acțiunile anterioare se vor crea trei clase: PourLoggerBefore, PourLoggerAfter și PourErrorLogger.

Și amintindu-ne că există trei operații pentru bețivi — ajungem la nouă clase de logare. În total, întreaga structură a bețivului constă din 14 (!!!) clase.

Hyperbola? Puțin probabil! Oamenii-maimuță cu grenade de decompoziție vor descompune “turnătorul” în carafe, pahare, operatori de turnare, servicii de furnizare a apei, modele fizice de coliziune a moleculelor, iar în următorul trimestru vor încerca să descurce dependențele fără variabile globale. Și să credeți — el nu se va opri.

Exact în acest punct, mulți ajung la concluzia că SRP este doar povești din regate roz, și pleacă să-și pună o pălărie...

… fără să afle vreodată despre existența celei de-a treia definiții a SRP:

„Principiul responsabilității unice afirmă că elementele care sunt similare în ceea ce privește modificarea ar trebui să fie păstrate într-un singur loc„. sau “Ceea ce se schimbă împreună ar trebui să fie păstrat într-un singur loc”

Asta înseamnă că, dacă schimbăm logarea operației, trebuie să facem acest lucru într-un singur loc.

Acesta este un punct foarte important — deoarece toate explicațiile SRP de mai sus afirmau că trebuie să descompunem tipurile până când se descompun, adică impune un „limite superioare” asupra dimensiunii obiectului, iar acum vorbim deja și despre un „limite inferioare”. Cu alte cuvinte, SRP nu numai că solicită „descompunerea până când se descompune”, dar trebuie să ne și menținem limita — „să nu descompunem lucruri legate”. Este o mare bătălie între lamela lui Occam și omul-maimuță!

Principiul responsabilității unice. Nu este atât de simplu cum pare

Acum, bețivului ar trebui să-i fie mai ușor. În afară de faptul că nu trebuie să descompună loggerul IPourLogger în trei clase, putem de asemenea să combinăm toate loggerele î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ă ne va adăuga un al patrulea tip de operațiune, atunci pentru acesta este deja pregătită logarea. Iar codul operațiunilor acestora este curat și scutit de zgomotul infrastructurii.

În rezultat avem 5 clase pentru a rezolva problema consumului:

  • Operația umplerii
  • Operația consumului
  • Operația asocierii
  • Logger-ul
  • Fațada consumatorului

Fiecare dintre ele răspunde strict pentru o funcționalitate, având un singur motiv pentru schimbare. Toate regulile similare pentru schimbare sunt aranjate împreună.

Exemplu din viața reală

Odată scriam un serviciu de înregistrare automată a clienților b2b. Și a apărut un metodă GOD de 200 de linii de conținut similar:

  • Du-te în 1C și deschide un cont
  • Cu acest cont, du-te la modulul de plată și deschide-l acolo
  • Verifică că un cont cu acest număr nu a fost creat în principal server
  • Creează un cont nou
  • Rezultatul înregistrării în modulul de plată și numărul 1C adaugă în serviciul rezultatelor înregistrării
  • Adaugă în această tabelă informațiile despre cont
  • Creează un număr de punct pentru acest client în serviciul punctelor. Transmite în acest serviciu numărul contului 1C.

Și în această listă au fost încă aproximativ 10 operațiuni de business cu o legătură teribilă. Obiectul contului era necesar aproape tuturor. Identificatorul punctului și numele clientului erau necesare în jumătate din apeluri.

După o oră de refactorizare, am reușit să separăm codul infrastructural și unele nuanțe de lucru cu contul în metode/clase separate. Metoda God s-a ușurat, dar au rămas 100 de linii de cod care nu voiau să se descleșteze.

Doar după câteva zile a venit înțelegerea că esența acestei metode „ușurate” este chiar algoritmul de business. Și că descrierea inițială a specificației a fost destul de complicată. Și exact încercarea de a împărți această metodă va constitui o încălcare a SRP, nu invers.

Formalism.

A venit timpul să lăsăm în pace pe consumatorul nostru. Ștergeți lacrimile - ne vom întoarce la el odată. Acum să formalizăm cunoștințele din acest articol.

Formalism 1. Definiția SRP

  1. Separati elementele astfel încât fiecare dintre ele să fie responsabil pentru ceva unic.
  2. Responsabilitatea se descifrează ca „motiv pentru schimbare”. Adică fiecare element are un singur motiv pentru schimbare, în termeni de logică de business.
  3. Schimbările potențiale ale logicii de afaceri trebuie să fie localizate. Elementele care se schimbă sincron trebuie să fie apropiate.

Formalism 2. Criteriile necesare de autoevaluare.

Nu am întâlnit criterii suficiente pentru îndeplinirea SRP. Dar există condiții necesare:

1) Puneți-vă întrebarea - ce face această clasă/metod/modul/serviciu. Trebuie să răspundeți la ea cu o definiție simplă. (mulțumesc Brightori )

explicații

Totuși, uneori este foarte greu să găsești o definiție simplă.

2) Fixarea unei anumite erori sau adăugarea unei noi funcționalități afectează un număr minim de fișiere/clase. Ideal - una.

explicații

Deoarece responsabilitatea (pentru funcționalitate sau eroare) este încapsulată într-un singur fișier/clasă, știți exact unde să căutați și ce să modificați. De exemplu: o funcționalitate care modifică logarea operațiunilor va necesita doar modificarea logger-ului. Nu este necesar să căutați în tot restul codului.

Un alt exemplu - adăugarea unui nou control UI, similar cu cele anterioare. Dacă acest lucru va necesita adăugarea a 10 entități diferite și 15 convertori diferite - pare că ați „supraîncărcat”.

3) Dacă mai mulți dezvoltatori lucrează la funcționalități diferite ale proiectului dumneavoastră, probabilitatea unui conflict de fuziune, adică probabilitatea ca același fișier/clasă să fie modificat de mai mulți dezvoltatori simultan - este minimă.

explicații

Dacă, la adăugarea unei noi operațiuni „Versarea votcii sub masă”, trebuie să afectați logger-ul, operațiunea de băut și versare - atunci pare că responsabilitățile sunt împărțite prost. Desigur, acest lucru nu este întotdeauna posibil, dar trebuie să încercați să reduceți acest indice.

4) La întrebările de clarificare despre logica de afaceri (de la dezvoltator sau manager) vă raziți strict într-o singură clasă/fișier și obțineți informații doar de acolo.

explicații

Funcționalitățile, regulile sau algoritmii sunt scriși compact, fiecare într-un singur loc, nu împrăștiați prin steaguri în tot spațiul de cod.

5) Numele sunt clare.

explicații

Clasa sau metoda noastră este responsabilă pentru ceva unic, iar responsabilitatea este reflectată în numele său.

AllManagersManagerService - cel mai probabil, este un God-class.
LocalPayment - probabil, nu.

Formalism 3. Metodologia de dezvoltare ‘Ockham-first’.

La începutul proiectării, maimuța umană nu știe și nu simte toate subtilitățile problemei pe care o rezolvă și poate greși. Poți greși în diferite moduri:

  • Crearea de obiecte prea mari, combinând diverse responsabilități
  • Fragmentarea, împărțind o responsabilitate unică în multe tipuri diferite
  • Definirea greșită a limitelor responsabilității

Este important să rețineți regula: „e mai bine să greșești în mod exagerat” sau „dacă nu ești sigur — nu fragmenta”. De exemplu, dacă clasa ta reunește două responsabilități — aceasta rămâne clară și o poți diviza în două cu modificări minime ale codului clientului. Crearea unui pahar din cioburi de sticlă este, de obicei, mai complicată din cauza contextului dispersat în mai multe fișiere și a lipsei dependențelor necesare în codul clientului.

Este timpul să încheiem

Domeniul de aplicare al SRP nu se limitează la OOP și SOLID. Acesta se aplică metodelor, funcțiilor, claselor, modulelor, microserviciilor și serviciilor. Este aplicabil atât în dezvoltarea „figaxs-figaxs-și-in-prod”, cât și în „războiul-rocket” de dezvoltare, făcând lumea puțin mai bună în fiecare loc. Dacă ne gândim, este cu siguranță un principiu fundamental al ingineriei. Ingineria mecanică, sistemele de control și toate sistemele complexe sunt construite din componente, iar „sub-fragmentarea” îi privează pe ingineri de flexibilitate, „supra-fragmentarea” — de eficiență, iar limitele greșite — de raționament și liniște sufletească.

Principiul responsabilității unice. Nu este atât de simplu cum pare

SRP nu este o invenție a naturii și nu face parte din știința exactă. El rezultă din limitele noastre biologice și psihologice. Este doar o modalitate de a controla și dezvolta sisteme complexe cu ajutorul creierului uman-maimuță. Ne învață cum să descompunem un sistem. Formularea inițială necesita o abilitate considerabilă de telepatie, dar sper că acest articol a risipit puțin ceața.

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster