Jedinstveni odgovorni princip. Nije tako jednostavno kao što se čini

Jedinstveni odgovorni princip. Nije tako jednostavno kao što se čini Princip jedinstvene odgovornosti, također poznat kao princip jedinstvene obaveze,
također poznat kao princip jedinstvene promjene — iznimno klizak za razumijevanje i vrlo nelagodno pitanje na intervjuu za programere.

Moje prvo ozbiljno uvjeravanje s tim principom dogodilo se na početku prve godine, kada su nas mlade i neiskusne odveli u šumu kako bi od nas stvorili prave studente.

U šumi su nas podijelili u grupe od 8-9 osoba i organizirali natjecanje — koja grupa će brže popiti bocu rakije pod uvjetom da prvi član grupe ulije rakiju u čašu, drugi pije, a treći grize. Izvršeni član suradnje odlazi na kraj reda grupe.

Slučaj kada je veličina reda bila višekratnik tri, bio je dobra realizacija SRP-a.

Definicija 1. Jedinstvena odgovornost.

Pravna definicija principa jedinstvene odgovornosti (SRP) kaže da svaki objekt ima svoju odgovornost i razlog postojanja, a ta odgovornost je samo jedna.

Razmotrite objekt „Pijanac“ (Tippler).
Za implementaciju SRP-a podijelit ćemo zaduženja na troje:

  • Jedan ulije (PourOperation)
  • Jedan pije (DrinkUpOperation)
  • Jedan grize (TakeBiteOperation)

Svaki od sudionika u procesu odgovoran je za jednu komponentu procesa, tj. ima jednu atomsku odgovornost — popiti, uliti ili gristi.

Pijanac, s druge strane, služi kao fasada za te operacije:

class Tippler {
    //...
    void Act(){
        _pourOperation.Do() // uliti
        _drinkUpOperation.Do() // popiti
        _takeBiteOperation.Do() // gristi
    }
}

Jedinstveni odgovorni princip. Nije tako jednostavno kao što se čini

Zašto?

Programer piše kod za čovjeka-majmun, a čovjek-majmun je nepažljiv, glup i stalno negdje žuri. Može zadržati i razumjeti otprilike 3 — 7 termina u jednom trenutku.
U slučaju pijanice, ti termini su tri. Međutim, ako napišemo kod kao jednu dugačku datoteku, pojavit će se ruke, čaše, pretučene osobe i beskrajne rasprave o politici. I sve to bit će u tijelu jedne metode. Siguran sam da ste vidjeli takav kod u svojoj praksi. Nije najhumanije iskušenje za psihu.

S druge strane, čovjek-majmuna posvećen je modeliranju objekata stvarnog svijeta u svojoj glavi. U svojoj mašti može ih sudarati, sastavljati nove objekte i na isti način ih razdvajati. Zamislite staru maketu automobila. Možete u mašti otvoriti vrata, odvrnuti oblogu vrata i vidjeti mehanizme prozora, unutar kojih će biti zupčanici. No, ne možete vidjeti sve komponente automobila odjednom, u jednom "popisu". Barems ne može "čovjek-majmuna".

Stoga programeri-dekompozitori razlažu složene mehanizme na set manje složenih i funkcionalnih elemenata. Međutim, dekompozicija se može raditi na različite načine: u mnogim starim automobilima - dovod zraka izlazi u vrata, dok u modernima, kvar elektronike brave onemogućava pokretanje motora, što predstavlja problem prilikom popravka.

Dakle, SRP je princip koji objašnjava KAKO dekomponirati, to jest gdje povući liniju razdvajanja..

Govori da se dekomponirati mora po principu razdvajanja "odgovornosti", to jest prema zadacima određenih objekata.

Jedinstveni odgovorni princip. Nije tako jednostavno kao što se čini

Vratimo se na pijanog čovjeka i prednosti koje dobiva čovjek-majmuna prilikom dekompozicije:

  • Kod je postao potpuno jasan na svakoj razini.
  • Kod mogu pisati više programera istovremeno (svaki piše zaseban element).
  • Pojednostavljuje se automatsko testiranje - što je element jednostavniji, to ga je lakše testirati.
  • Pojavljuje se kompozicionalnost koda - možete zamijeniti DrinkUpOperation operaciju u kojoj pijanica prosipa tekućinu ispod stola. Ili zamijeniti operaciju točenja s operacijom u kojoj miješate vino i vodu ili vodku i pivo. Ovisno o poslovnim zahtjevima, možete učiniti sve to, a da pritom ne dirate kod metode. Tippler.Act.
  • Od ovih operacija možete sastaviti prejedaju (koristeći samo TakeBitOperation), alkoholičara (koristeći samo DrinkUpOperation direktno iz boce) i zadovoljiti mnoge druge poslovne zahtjeve.

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

I naravno, nedostaci:

  • Morat ćete stvoriti više tipova.
  • Pijanica će prvi put popiti nekoliko sati kasnije nego što bi mogao.

Definicija 2. Jedinstvena promjenjivost.

Dopustite mi, gospodo! Klasa pića ima jednu jedinu odgovornost — popiti! I uopće, riječ "odgovornost" je izrazito mutna. Netko je odgovoran za sudbinu čovječanstva, a netko drugi za podizanje prevrnutih pingvina na polu.

Razmotrite dvije implementacije pića. Prva, ranije navedena, sadrži tri klase — uliti, popiti i grickati.

Druga, napisana metodologijom "Naprijed i samo naprijed" sadrži svu logiku u metodi 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);
    }
   }
}

Obje ove klase, s gledišta vanjskog promatrača, izgledaju apsolutno identično i imaju jedinstvenu odgovornost "popiti".

Neugodno!

Tada zaronimo u internet i saznamo drugu definiciju SRP — Princip jedinstvene promjene (Single Changeability Principle).

SCP kaže da "Modul ima samo jedan i jedini razlog za promjenu". To jest "Odgovornost je razlog za promjenu".

(Izgleda da su momci koji su smislili prvotnu definiciju bili uvjereni u telepatske sposobnosti čovjeka-majmuna)

Sada se sve slaže. Odvojeno možemo mijenjati procese ulijevanja, pijenja i grickanja, a u samom piću možemo promijeniti samo redoslijed i sastav operacija, na primjer, premještanjem grickalice ispred pijenja ili dodavanjem čitanja toasta.

U pristupu "Naprijed i samo naprijed", sve što se može promijeniti — mijenja se samo u metodi Act. To može biti čitljivo i učinkovito kada je logike malo i rijetko se mijenja, ali često završava strašnim metodama od 500 linija, s brojem if -ova većim nego što je potrebno za pristup Rusije u NATO.

Definicija 3. Lokalizacija promjena.

Pijana osoba često ne razumije zašto se probudila u tuđem stanu, ili gdje joj je mobitel. Došlo je vrijeme da dodamo detaljno logiranje.

Započnimo logiranje s procesom ulijevanja:

class PourOperation: IOperation{
    PourOperation(ILogger log /*....*/){/*...*/}
    //...
    void Do(){
        _log.Log($"Prije ulijevanja s {_hand} i {_bottle}");
        //Poslovna logika ulijevanja ...
        _log.Log($"Nakon ulijevanja s {_hand} i {_bottle}");
    }
}

Inkapuliravši je u PourOperation, postupili smo mudro s obzirom na odgovornost i enkapsulaciju, ali imamo problem s principom promjenljivosti. Osim same operacije koja se može mijenjati, promjenljiva postaje i sama logika. Trebat ćemo podijeliti i stvoriti posebni logger za operaciju točenja:

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{
             //... poslovna logika
             _log.LogAfter(_hand, _bottle);
        }
        catch(exception e){
            _log.OnError(_hand, _bottle, e);
        }
    }
}

Pažljiv čitatelj primijetit će da LogAfter, LogBefore i OnError također se mogu mijenjati pojedinačno, a po analogiji s prethodnim postupcima će stvoriti tri klase: PourLoggerBefore, PourLoggerAfter i PourErrorLogger.

A s obzirom na to da postoje tri operacije točenja, dobivamo devet klasa logiranja. Tako da cijeli sustav točenja sastoji se od 14 (!!!) klasa.

Hipoteka? Jedva! Čovjek-majmuna s dekompozicijskom granatom rasparča 'točitelja' na bokal, čašu, operatore točenja, servis za delivery vode, fizički model sudara molekula, a sljedeći kvartal će pokušavati raspetljati ovisnosti bez globalnih varijabli. I vjerujte — neće stati.

U ovom trenutku mnogi dolaze do zaključka da je SRP samo bajka iz ružičastih kraljevstava, i odlaze zaplesti priču...

… a da nikada ne saznaju o postojanju treće definicije SRP:

"Princip jedinstvene odgovornosti kaže da slične stvari za promjenu trebaju biti pohranjene na jednom mjestu" ili "Ono što se mijenja zajedno, treba biti pohranjeno na jednom mjestu”

To jest, ako mijenjamo logiku operacije, trebali bismo to mijenjati na jednom mjestu.

To je vrlo važna točka — jer su svi raniji opisi SRP-a govorili o tome da trebamo razdvajati tipove dok se mogu razdvojiti, što je postavilo 'gornje ograničenje' na veličinu objekta, a sada govorimo i o 'donjem ograničenju'. Drugim riječima, SRP ne samo da zahtijeva 'razdvajanje dok se može razdvajati', nego i da se ne pretjeruje — 'ne razdvojiti povezane stvari'. To je velika bitka Ockamove oštrice s čovjekom-majmunom!

Jedinstveni odgovorni princip. Nije tako jednostavno kao što se čini

Sada bi točitelju trebalo biti lakše. Osim toga, što ne treba razdvajati logger IPourLogger na tri klase, također možemo spojiti sve loggere u jedan 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 ako nam se doda četvrti tip operacije, onda je logiranje već spremno. A kod samih operacija je čist i oslobođen infrastrukturne buke.

U rezultatu imamo 5 klasa za rješavanje problema točenja:

  • Operacija točenja
  • Operacija ispijanja
  • Operacija grickanja
  • Loger
  • Facade točenja

Svaka od njih odgovara isključivo za jednu funkcionalnost i ima jedan razlog za promjenu. Sva slična pravila za promjenu su blizu jedni drugima.

Primjer iz stvarnog života

Jednom smo radili servis za automatsku registraciju b2b klijenta. Pojavila se GOD-metoda sa 200 redaka takvog sadržaja:

  • Idi u 1C i otvori račun
  • S tim računom idi do modula za plaćanje i otvori ga tamo
  • Provjeri da račun s takvim brojem nije već napravljen u glavnom serveru
  • Kreiraj novi račun
  • Rezultat registracije u modulu za plaćanje i broj 1C dodaj u servis rezultata registracije
  • Dodaj u ovu tabelu informacije o računu
  • Kreiraj broj točke za ovog klijenta u servisu točaka. Prenesi u taj servis broj računa 1C.

I bilo je u ovom popisu još oko 10 poslovnih operacija s strašnom povezanošću. Objekt računa bio je potreban gotovo svima. Identifikator točke i ime klijenta bili su potrebni u polovini poziva.

Nakon sat vremena refaktorizacije, uspjeli smo odvojiti infrastrukturni kod i neke nijanse rada s računom u posebne metode/klase. God metoda se olakšala, ali ostalo je 100 redaka koda koji se nikako nisu željeli rasplesti.

Tek nakon nekoliko dana došlo je razumijevanje da je suština te „olakšane“ metode — poslovni algoritam. I da je izvorni opis TŽ bio prilično složen. I upravo će pokušaj razbijanja ove metode na dijelove narušiti SRP, a ne obrnuto.

Formalizam.

Došlo je vrijeme da ostavimo našu točionicu na miru. Obriši suze — sigurno ćemo se ponovno vratiti njemu nekada. A sada formalizirajmo znanje iz ovog članka.

Formalizam 1. Definicija SRP

  1. Dijelite elemente tako da svaki od njih bude odgovoran za nešto jedno.
  2. Odgovornost se objašnjava kao „uzrok za promjenu“. To jest, svaki element ima samo jedan uzrok za promjenu, u terminima poslovne logike.
  3. Potencijalne promjene poslovne logike trebaju biti lokalizirane. Promjenjivi sinkronizirani elementi trebaju biti blizu.

Formalizam 2. Potrebni kriteriji samoprovjere.

Nisam se susreo s dovoljnim kriterijima za provedbu SRP. No postoje potrebni uvjeti:

1) Postavite si pitanje — što radi ova klasa/metoda/modul/servis. Na njega trebate odgovoriti jednostavnom definicijom. (hvala Brightori )

objašnjenja

U svakom slučaju, ponekad je izuzetno teško pronaći jednostavnu definiciju.

2) Ispravak određenog buga ili dodavanje nove funkcionalnosti utječe na minimalan broj datoteka/klasa. U idealnom slučaju — jednu.

objašnjenja

S obzirom na to da je odgovornost (za funkcionalnost ili bug) inkapsulirana u jednoj datoteci/klasi, točno znate gdje potražiti i što promijeniti. Na primjer: funkcionalnost promjene izlaza logiranja operacija zahtijeva promjenu samo loggera. Nije potrebno pretraživati ostatak koda.

Drugi primjer — dodavanje novog UI kontrola, sličnog prethodnima. Ako vas to natjera da dodate 10 različitih entiteta i 15 različitih konvertera — čini se da ste "preopterećeni".

3) Ako nekoliko programera radi na različitim funkcionalnostima vašeg projekta, vjerojatnost sukoba pri spajanju, odnosno vjerojatnost da će istu datoteku/klasu izmijeniti nekoliko programera istovremeno — minimalna je.

objašnjenja

Ako prilikom dodavanja nove operacije "Ispijanje vodke ispod stola" trebate dirati logger, operaciju ispijanja i izlijevanja — onda izgleda da su odgovornosti nerazmjerno podijeljene. Naravno, to nije uvijek moguće, ali trebate nastojati smanjiti ovaj postotak.

4) Na pojašnjavajuće pitanje o poslovnoj logici (od programera ili menadžera) ulazite strogo u jednu klasu/datoteku i dobivate informacije samo odatle.

objašnjenja

Funkcionalnosti, pravila ili algoritmi kompaktno su napisani svaki na jednom mjestu, a ne razbacani zastavicama po cijelom kodu.

5) Ime je jasno.

objašnjenja

Naša klasa ili metoda odgovorna je za nešto jedno, i odgovornost je odražena u njenom nazivu.

AllManagersManagerService — vjerojatno, Bog-klasa.
LocalPayment — vjerojatno ne.

Formalizam 3. Metodologija razvoja „Ockham-prvo“.

Na početku projektiranja, čovjek-majmuna ne zna i ne osjeća sve nijanse riješenog zadatka i može napraviti grešku. Greške mogu nastati na različite načine:

  • Prevelike objekte napraviti spajanjem različitih odgovornosti.
  • Razložiti jedinstvenu odgovornost na mnogo različitih tipova.
  • Pogrešno definirati granice odgovornosti.

Važno je zapamtiti pravilo: "bolje je pogriješiti na najširem području", ili "ako niste sigurni — ne razdvajajte". Na primjer, ako vaša klasa sadrži dvije odgovornosti — ona i dalje ostaje jasna i može se razdvojiti na dvije s minimalnom izmjenom klijentskog koda. Skupljati čašu od staklenih komadića obično je teže zbog raspršenog konteksta među više datoteka i nedostatka potrebnih ovisnosti u klijentskom kodu.

Vrijeme je da se zaključi.

Primjena SRP-a ne ograničava se na OOP i SOLID. Primjenjuje se na metode, funkcije, klase, module, mikrousluge i servise. Primjenjuje se i na "figaks-figaks-i-u-produkciji" i "raketna znanost" razvoj, čineći svijet malo boljim svuda. Ako dobro razmislimo, to je vjerojatno temeljni princip cijelog inženjerstva. Strojogradnja, sustavi upravljanja, pa i svi složeni sustavi — grade se od komponenata, a "nedovoljno razdvajanje" lišava konstruatore fleksibilnosti, "prekomjerno razdvajanje" — učinkovitosti, dok pogrešne granice oduzimaju razboritost i duševni mir.

Jedinstveni odgovorni princip. Nije tako jednostavno kao što se čini

SRP nije izumljen prirodom i nije dio točne znanosti. Izražava se iz naših bioloških i psiholoških ograničenja. To je samo način kontrole i razvoja složenih sustava koristeći ljudski um. Objašnjava nam kako dekomponirati sustav. Izvorna formulacija zahtijevala je određenu vještinu telepatije, ali se nadam da je ovaj članak donekle raspucao dimnu zavjesu.

Izvor: habr.com

Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere 🔥 Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere | ProHoster