Načelo jedinstvene odgovornosti. Nije tako jednostavno kako se čini

Načelo jedinstvene odgovornosti. Nije tako jednostavno kako se čini Načelo jedinstvene odgovornosti, poznato i kao načelo jedinstvene odgovornosti,
aka princip uniformne varijabilnosti - izuzetno sklizak tip za razumijevanje i tako nervozno pitanje na razgovoru za programera.

Moje prvo ozbiljnije upoznavanje s tim principom dogodilo se početkom prve godine, kada su mlade i zelene odveli u šumu da od ličinki naprave učenike – prave učenike.

U šumi smo se podijelili u grupe od po 8-9 ljudi i natjecali se - koja će grupa brže popiti bocu votke, s tim da prvi iz grupe natoči votku u čašu, drugi je popije, a treći ima užinu. Jedinica koja je završila svoju operaciju pomiče se na kraj reda čekanja grupe.

Slučaj u kojem je veličina reda bila višekratnik tri bila je dobra implementacija SRP-a.

Definicija 1. Pojedinačna odgovornost.

Službena definicija načela jedinstvene odgovornosti (SRP) kaže da svaki entitet ima svoju odgovornost i razlog postojanja, a ima samo jednu odgovornost.

Razmotrite objekt "Pijač" (Bekrija).
Za provedbu načela SRP-a, podijelit ćemo odgovornosti u tri:

  • Jedan toči (PourOperation)
  • Jedno piće (DrinkUpOperation)
  • Jedan ima užinu (TakeBiteOperation)

Svaki od sudionika u procesu odgovoran je za jednu komponentu procesa, odnosno ima jednu atomsku odgovornost - piti, natočiti ili grickati.

Otvor za piće je, pak, fasada za ove operacije:

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

Načelo jedinstvene odgovornosti. Nije tako jednostavno kako se čini

Zašto?

Ljudski programer piše kod za čovjeka-majmuna, a čovjek-majmun je nepažljiv, glup i uvijek u žurbi. Može držati i razumjeti oko 3 - 7 pojmova odjednom.
U slučaju pijanice postoje tri ova pojma. No, ako kodeks napišemo jednim listom, onda će on sadržavati ruke, čaše, tučnjave i beskonačne prepirke o politici. I sve će to biti u tijelu jedne metode. Siguran sam da ste vidjeli takav kod u svojoj praksi. Nije najhumaniji test za psihu.

S druge strane, čovjek majmun dizajniran je da u svojoj glavi simulira objekte iz stvarnog svijeta. U mašti ih može gurati, sastavljati od njih nove predmete i na isti ih način rastavljati. Zamislite stari model automobila. U svojoj mašti možete otvoriti vrata, odvrnuti oblogu vrata i tamo vidjeti mehanizme za podizanje prozora unutar kojih će biti zupčanici. Ali ne možete vidjeti sve komponente stroja u isto vrijeme, u jednom "listingu". Barem “čovjek-majmun” ne može.

Stoga ljudski programeri rastavljaju složene mehanizme u skup manje složenih i radnih elemenata. No, može se razgraditi na različite načine: u mnogim starim automobilima zračni kanal ulazi u vrata, au modernim automobilima kvar na elektronici brave onemogućuje pokretanje motora, što može biti problem prilikom popravka.

sada, SRP je princip koji objašnjava KAKO dekomponirati, odnosno gdje povući crtu razdjelnice.

Kaže da je potrebno razgraditi po principu podjele “odgovornosti”, odnosno po zadaćama pojedinih objekata.

Načelo jedinstvene odgovornosti. Nije tako jednostavno kako se čini

Vratimo se piću i prednostima koje čovjek majmun dobiva tijekom razgradnje:

  • Kodeks je postao iznimno jasan na svakoj razini
  • Kod može pisati više programera odjednom (svaki piše zaseban element)
  • Automatizirano testiranje je pojednostavljeno - što je element jednostavniji, to ga je lakše testirati
  • Pojavljuje se kompozicija koda - možete zamijeniti DrinkUpOperation na operaciju u kojoj pijanac izlijeva tekućinu pod stol. Ili zamijenite operaciju točenja s operacijom u kojoj miješate vino i vodu ili votku i pivo. Ovisno o poslovnim zahtjevima, možete učiniti sve bez diranja koda metode Tippler.Djeluj.
  • Iz ovih operacija možete preklopiti proždrljivca (samo pomoću TakeBitOperation), Alkoholičar (samo korištenje DrinkUpOperation izravno iz boce) i zadovoljiti mnoge druge poslovne zahtjeve.

(Oh, čini se da je ovo već OCP princip, a ja sam prekršio odgovornost ovog posta)

I, naravno, mane:

  • Morat ćemo stvoriti više vrsta.
  • Pijanica prvi put pije nekoliko sati kasnije nego što bi inače.

Definicija 2. Jedinstvena varijabilnost.

Dopustite, gospodo! Razred pijenja također ima jednu odgovornost - pije! I općenito, riječ "odgovornost" je vrlo nejasan koncept. Netko je odgovoran za sudbinu čovječanstva, a netko je odgovoran za podizanje pingvina koji su se prevrnuli na stup.

Razmotrimo dvije izvedbe pojilice. Prvi, gore spomenuti, sadrži tri klase - točenje, piće i snack.

Drugi je napisan kroz metodologiju “Naprijed i samo naprijed” i sadrži svu logiku u metodi čin:

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

Obje ove klase, sa stajališta vanjskog promatrača, izgledaju potpuno isto i dijele istu odgovornost "pijenja".

Zbunjenost!

Zatim idemo na internet i pronalazimo još jednu definiciju SRP-a - načelo jedinstvene promjenjivosti.

SCP navodi da "Modul ima jedan i samo jedan razlog za promjenu". Odnosno, "Odgovornost je razlog za promjenu."

(Čini se da su dečki koji su smislili originalnu definiciju bili uvjereni u telepatske sposobnosti čovjeka majmuna)

Sad sve sjeda na svoje mjesto. Zasebno možemo promijeniti postupke točenja, pijenja i grickanja, ali u samoj pojilici možemo promijeniti samo redoslijed i sastav operacija, na primjer, pomicanjem zalogaja prije pijenja ili dodavanjem čitanja zdravice.

U pristupu “Naprijed i samo naprijed” sve što se može promijeniti mijenja se samo u metodi čin. Ovo može biti čitljivo i učinkovito kada ima malo logike i rijetko se mijenja, ali često završi u užasnim metodama od po 500 redaka, s više ako-izjava nego što je potrebno da se Rusija pridruži NATO-u.

Definicija 3. Lokalizacija promjena.

Pijancima često nije jasno zašto su se probudili u tuđem stanu, niti gdje im je mobitel. Vrijeme je da dodate detaljnu evidenciju.

Počnimo bilježiti s postupkom izlijevanja:

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

Inkapsuliranjem u PourOperation, mudro smo postupili sa stajališta odgovornosti i enkapsulacije, ali sada smo zbunjeni principom varijabilnosti. Osim same operacije, koja se može promijeniti, promjenjivo postaje i samo evidentiranje. Morat ćete odvojiti i stvoriti poseban logger za operaciju izlijevanja:

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

Pedantan čitatelj će to primijetiti LogAfter, Log Prije и OnError također se može mijenjati pojedinačno, i, analogno prethodnim koracima, stvorit će tri klase: PourLoggerBefore, PourLoggerAfter и PourErrorLogger.

I imajući na umu da postoje tri operacije za piće, dobivamo devet klasa sječe. Kao rezultat toga, cijeli krug pijenja sastoji se od 14 (!!!) klasa.

Hiperbola? Jedva! Čovjek-majmun s granatom za raspadanje podijelit će “izlijevač” na dekanter, čašu, operatere za točenje, uslugu vodoopskrbe, fizikalni model sudara molekula, a iduće će tromjesečje pokušati razmrsiti ovisnosti bez globalne varijable. I vjerujte mi, neće stati.

Upravo na tom mjestu mnogi zaključe da su SRP bajke iz ružičastih kraljevstava i odu se igrati nudle...

... a da nikada nisam saznao za postojanje treće definicije Srp-a:

“Načelo jedinstvene odgovornosti to navodi stvari koje su slične mijeni trebaju biti pohranjene na jednom mjestu". ili "Ono što se zajedno mijenja treba držati na jednom mjestu"

Odnosno, ako promijenimo bilježenje operacije, moramo ga promijeniti na jednom mjestu.

Ovo je vrlo važna točka - budući da su sva objašnjenja SRP-a koja su bila gore govorila da je potrebno smrviti tipove dok se drobe, odnosno nametnuli su "gornju granicu" veličine objekta, a sada već govorimo o “donjoj granici” . Drugim riječima, SRP ne samo da zahtijeva "gnječenje dok se drobi", već i da ne treba pretjerivati ​​- "ne gnječite međusobno spojene stvari". Ovo je velika bitka između Occamove britve i čovjeka majmuna!

Načelo jedinstvene odgovornosti. Nije tako jednostavno kako se čini

Sada bi se osoba koja pije trebala osjećati bolje. Osim što nema potrebe dijeliti IPourLogger logger u tri klase, također možemo kombinirati sve loggere u jednu vrstu:

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

A ako dodamo četvrtu vrstu operacije, tada je zapisivanje za nju već spremno. A kod samih operacija je čist i bez infrastrukturne buke.

Kao rezultat toga, imamo 5 klasa za rješavanje problema s pićem:

  • Operacija izlijevanja
  • Operacija pijenja
  • Operacija ometanja
  • Drvosječa
  • Fasada pojilice

Svaki od njih odgovoran je isključivo za jednu funkcionalnost i ima jedan razlog za promjenu. Sva pravila slična promjenama nalaze se u blizini.

Primjer iz stvarnog života

Jednom smo napisali uslugu za automatsku registraciju b2b klijenta. I pojavila se GOD metoda za 200 redaka sličnog sadržaja:

  • Idite na 1C i kreirajte račun
  • S ovim računom idite na modul plaćanja i tamo ga kreirajte
  • Provjerite da račun s takvim računom nije kreiran na glavnom poslužitelju
  • Izraditi novi račun
  • Dodajte rezultate registracije u modul za plaćanje i 1c broj u servis rezultata registracije
  • Dodajte podatke o računu u ovu tablicu
  • Stvorite broj točke za ovog klijenta u usluzi bodova. Proslijedite svoj broj računa 1c ovoj usluzi.

A bilo je još oko 10 poslovnih operacija na ovom popisu s užasnom povezanošću. Gotovo svi su trebali računski objekt. ID točke i ime klijenta bili su potrebni u polovici poziva.

Nakon sat vremena refaktoriranja, uspjeli smo razdvojiti infrastrukturni kod i neke od nijansi rada s računom u zasebne metode/klase. Božja metoda je to olakšala, ali ostalo je 100 redaka koda koji se jednostavno nisu htjeli raspetljati.

Tek nakon nekoliko dana postalo je jasno da je bit ove “lagane” metode poslovni algoritam. I da je izvorni opis tehničkih specifikacija bio prilično složen. A upravo će pokušaj razbijanja ove metode na dijelove prekršiti SRP, a ne obrnuto.

Formalizam.

Vrijeme je da ostavimo našeg pijanca na miru. Obriši suze - sigurno ćemo mu se jednom vratiti. Sada formalizirajmo znanje iz ovog članka.

Formalizam 1. Definicija SRP-a

  1. Odvojite elemente tako da svaki od njih bude odgovoran za jednu stvar.
  2. Odgovornost označava "razlog za promjenu". Odnosno, svaki element ima samo jedan razlog za promjenu, u smislu poslovne logike.
  3. Moguće promjene poslovne logike. mora biti lokaliziran. Elementi koji se sinkrono mijenjaju moraju biti u blizini.

Formalizam 2. Neophodni kriteriji samoprovjere.

Nisam vidio dovoljno kriterija za ispunjavanje SRP-a. Ali postoje potrebni uvjeti:

1) Zapitajte se što ova klasa/metoda/modul/usluga radi. morate odgovoriti jednostavnom definicijom. ( Hvala vam Brightori )

objašnjenja

Međutim, ponekad je vrlo teško pronaći jednostavnu definiciju

2) Ispravljanje pogreške ili dodavanje nove značajke utječe na minimalni broj datoteka/klasa. U idealnom slučaju - jedan.

objašnjenja

Budući da je odgovornost (za značajku ili grešku) sadržana u jednoj datoteci/klasi, točno znate gdje tražiti i što urediti. Na primjer: značajka promjene izlaza operacija zapisivanja zahtijevat će promjenu samo zapisivača. Nema potrebe prolaziti kroz ostatak koda.

Drugi primjer je dodavanje nove kontrole korisničkog sučelja, slične prethodnima. Ako vas to prisiljava da dodate 10 različitih entiteta i 15 različitih pretvarača, izgleda da ste pretjerali.

3) Ako nekoliko programera radi na različitim značajkama vašeg projekta, tada je vjerojatnost sukoba spajanja, odnosno vjerojatnost da će istu datoteku/klasu promijeniti nekoliko programera u isto vrijeme, minimalna.

objašnjenja

Ako pri dodavanju nove operacije „Toči votku ispod stola“ trebate utjecati na drvosječu, operaciju ispijanja i točenja, onda izgleda da su odgovornosti podijeljene krivo. Naravno, to nije uvijek moguće, ali treba pokušati smanjiti tu brojku.

4) Kada vam se postavi razjašnjavajuće pitanje o poslovnoj logici (od programera ili upravitelja), idete striktno u jednu klasu/datoteku i primate informacije samo od tamo.

objašnjenja

Značajke, pravila ili algoritmi napisani su kompaktno, svaki na jednom mjestu, a ne razbacani zastavicama po prostoru koda.

5) Imenovanje je jasno.

objašnjenja

Naša klasa ili metoda odgovorna je za jednu stvar, a odgovornost se odražava u njenom nazivu

AllManagersManagerService - najvjerojatnije Božja klasa
LocalPayment - vjerojatno ne

Formalizam 3. Occam-prva metodologija razvoja.

Na početku dizajna čovjek majmun ne zna i ne osjeća sve suptilnosti problema koji se rješava i može pogriješiti. Možete pogriješiti na različite načine:

  • Učinite objekte prevelikima spajanjem različitih odgovornosti
  • Preoblikovanje dijeljenjem jedne odgovornosti na mnogo različitih vrsta
  • Netočno definirati granice odgovornosti

Važno je zapamtiti pravilo: "bolje je napraviti veliku pogrešku" ili "ako nisi siguran, nemoj se razilaziti". Ako, na primjer, vaša klasa sadrži dvije odgovornosti, onda je još uvijek razumljiva i može se podijeliti na dvije uz minimalne promjene koda klijenta. Sastavljanje čaše od krhotina stakla obično je teže zbog konteksta koji je raširen na nekoliko datoteka i nedostatka potrebnih ovisnosti u kodu klijenta.

Vrijeme je da završimo

Opseg SRP-a nije ograničen na OOP i SOLID. Primjenjuje se na metode, funkcije, klase, module, mikroservise i usluge. Primjenjuje se i na "figax-figax-and-prod" i na "rocket-science" razvoj, čineći svijet posvuda malo boljim. Ako bolje razmislite, to je gotovo temeljno načelo cjelokupnog inženjerstva. Strojarstvo, sustavi upravljanja i zapravo svi složeni sustavi izgrađeni su od komponenti, a "nedovoljna fragmentacija" dizajnerima oduzima fleksibilnost, "pretjerana fragmentacija" dizajnerima oduzima učinkovitost, a netočne granice im oduzimaju razum i duševni mir.

Načelo jedinstvene odgovornosti. Nije tako jednostavno kako se čini

SRP nije izmislila priroda i nije dio egzaktne znanosti. Izbija iz naših bioloških i psiholoških ograničenja. To je samo način kontrole i razvoja složenih sustava pomoću mozga čovjekolikog majmuna. On nam govori kako razgraditi sustav. Izvorna formulacija zahtijevala je priličnu količinu telepatije, ali nadam se da će ovaj članak ukloniti dio dimne zavjese.

Izvor: www.habr.com

Dodajte komentar