Princip jedinstvene odgovornosti. Nije tako jednostavno kao što se čini

Princip jedinstvene odgovornosti. Nije tako jednostavno kao što se čini Princip jedinstvene odgovornosti, poznat i kao princip jedinstvene odgovornosti,
aka princip uniformne varijabilnosti - izuzetno klizav tip za razumijevanje i tako nervozno pitanje na intervjuu za programera.

Moje prvo ozbiljnije upoznavanje sa ovim principom dogodilo se početkom prve godine, kada su mladi i zeleni odvedeni u šumu da od larvi prave učenike – prave studente.

U šumi smo se podijelili u grupe od po 8-9 ljudi i imali smo takmičenje - koja će grupa najbrže popiti flašu votke, s tim da prvi iz grupe sipa votku u čašu, drugi je, a treći ima užinu. Jedinica koja je završila svoju operaciju prelazi na kraj reda grupe.

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

Definicija 1. Jedinstvena odgovornost.

Zvanična definicija principa jedinstvene odgovornosti (SRP) kaže da svaki entitet ima sopstvenu odgovornost i razlog postojanja, te ima samo jednu odgovornost.

Razmotrite objekat "Pijač" (Tipler).
Da bismo implementirali princip SRP-a, odgovornosti ćemo podijeliti na tri:

  • Jedan sipa (PourOperation)
  • jedno piće (DrinkUpOperation)
  • Jedan ima užinu (TakeBiteOperation)

Svaki od učesnika u procesu odgovoran je za jednu komponentu procesa, odnosno ima jednu atomsku odgovornost - da pije, sipa ili gricka.

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

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

Princip jedinstvene odgovornosti. Nije tako jednostavno kao što 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 od ovih pojmova. Međutim, ako napišemo kod sa jednim listom, onda će on sadržavati ruke, čaše, tuče i beskrajne rasprave 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 je dizajniran da simulira objekte iz stvarnog svijeta u svojoj glavi. U svojoj mašti može ih gurnuti zajedno, sastaviti nove predmete od njih i rastaviti ih na isti način. 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 mašine u isto vrijeme, u jednom "listingu". Barem ne može “čovek majmun”.

Stoga, ljudski programeri razlažu složene mehanizme na skup manje složenih i radnih elemenata. Međutim, može se razgraditi na različite načine: kod mnogih starih automobila zračni kanal ide u vrata, a kod modernih automobila kvar elektronike brave onemogućava pokretanje motora, što može predstavljati problem prilikom popravke.

Sada, SRP je princip koji objašnjava KAKO razložiti, odnosno gdje povući liniju razdvajanja.

Kaže da je potrebno dekomponovati po principu podjele “odgovornosti”, odnosno prema zadacima pojedinih objekata.

Princip jedinstvene odgovornosti. Nije tako jednostavno kao što se čini

Vratimo se piću i prednostima koje čovek majmun dobija tokom raspadanja:

  • Kod je postao izuzetno jasan na svakom nivou
  • Kod može pisati nekoliko programera odjednom (svaki piše zaseban element)
  • Automatsko testiranje je pojednostavljeno - što je element jednostavniji, lakše ga je testirati
  • Pojavljuje se kompozicija koda - možete zamijeniti DrinkUpOperation na operaciju u kojoj pijanica sipa tečnost ispod stola. Ili zamijenite operaciju točenja operacijom u kojoj miješate vino i vodu ili votku i pivo. Ovisno o poslovnim zahtjevima, sve možete učiniti bez dodirivanja koda metode Tipler.Act.
  • Iz ovih operacija možete presavijati proždrljivu (samo pomoću TakeBitOperation), Alkohol (samo za upotrebu DrinkUpOperation direktno iz boce) i ispunjavaju mnoge druge poslovne zahtjeve.

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

I, naravno, nedostaci:

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

Definicija 2. Jedinstvena varijabilnost.

Dozvolite mi, gospodo! Čas pijenja takođe ima jednu odgovornost - pije! I općenito, riječ "odgovornost" je krajnje nejasan pojam. Neko je odgovoran za sudbinu čovečanstva, a neko je odgovoran za podizanje pingvina koji su prevrnuti na motku.

Razmotrimo dvije implementacije pojilice. Prvi, gore pomenuti, sadrži tri klase - pour, drink i snack.

Drugi je napisan kroz metodologiju „Naprijed i samo naprijed“ i sadrži svu logiku metode djelo:

//Не тратьте время  на изучение этого класса. Лучше съешьте печеньку
с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 stanovišta vanjskog posmatrača, izgledaju potpuno isto i dijele istu odgovornost za “pijanje”.

Konfuzija!

Zatim idemo na internet i saznajemo još jednu definiciju SRP-a - princip jedne promjenjivosti.

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

(Čini se da su momci koji su smislili originalnu definiciju bili sigurni u telepatske sposobnosti čovjeka majmuna)

Sada sve dolazi na svoje mjesto. Zasebno možemo promijeniti postupke točenja, pijenja i grickanja, ali u samom pojilu možemo samo promijeniti redoslijed i sastav radnji, na primjer, pomjeranjem zalogaja prije pijenja ili dodavanjem čitanja zdravice.

U pristupu „Naprijed i samo naprijed“, sve što se može promijeniti mijenja se samo u metodi djelo. Ovo može biti čitljivo i efikasno kada je malo logike i rijetko se mijenja, ali često završi u strašnim metodama od po 500 redova, s više if-izjava nego što je potrebno da bi se Rusija pridružila NATO-u.

Definicija 3. Lokalizacija promjena.

Pijači često ne razumiju zašto su se probudili u tuđem stanu, niti gdje im je mobilni telefon. Vrijeme je da dodate detaljnu evidenciju.

Započnimo zapisnik s procesom izlivanja:

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 PourOperation, postupili smo mudro sa stanovišta odgovornosti i inkapsulacije, ali sada smo zbunjeni principom varijabilnosti. Osim same operacije, koja se može promijeniti, promjenjivo postaje i samo evidentiranje. Morat ćete odvojiti i kreirati poseban loger 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)
        }
    }
}

Pažljivi čitalac će to primijetiti LogAfter, LogBefore и OnError može se mijenjati i pojedinačno, i, po analogiji s prethodnim koracima, kreirat će se tri klase: PourLoggerBefore, PourLoggerAfter и PourErrorLogger.

A sjetimo se da su tri operacije za pojilicu, dobijemo devet časova sječe. Kao rezultat, cijeli krug pijenja sastoji se od 14 (!!!) časova.

Hiperbola? Teško! Čovek majmun sa granatom za razgradnju će podeliti „sipač“ na dekanter, čašu, operatere za točenje, vodovod, fizički model sudara molekula, a u sledećem kvartalu pokušaće da razmrsi zavisnosti bez globalne varijable. I vjerujte mi, neće stati.

U ovom trenutku mnogi dolaze do zaključka da su SRP bajke iz ružičastih kraljevstava i odlaze da se igraju nudle...

... a da nikad nisam saznao za postojanje treće definicije srp-a:

“Princip jedinstvene odgovornosti to kaže stvari koje su slične promjeni treba čuvati na jednom mjestu". ili "Ono što se zajedno mijenja treba držati na jednom mjestu"

To jest, ako promijenimo evidentiranje operacije, onda ga moramo promijeniti na jednom mjestu.

Ovo je vrlo važna stvar - budući da su sva objašnjenja SRP-a koja su navedena iznad govorila da je potrebno tipove zgnječiti 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 zahteva „gnječenje dok drobite“, već i da ne preterujete – „ne lomite stvari koje su međusobno povezane“. Ovo je velika bitka između Occamovog brijača i čovjeka majmuna!

Princip jedinstvene odgovornosti. Nije tako jednostavno kao što se čini

Sada bi se onaj koji pije trebao osjećati bolje. Osim što nema potrebe da IPourLogger logger dijelimo u tri klase, možemo i kombinirati sve logere 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){/*..*/}
}

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

Kao rezultat, imamo 5 časova za rješavanje problema pijenja:

  • Operacija izlivanja
  • Operacija za piće
  • Operacija ometanja
  • Logger
  • Fasada za piće

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

Primjer iz stvarnog života

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

  • Idite na 1C i kreirajte nalog
  • Sa ovim nalogom idite na modul plaćanja i kreirajte ga tamo
  • Proverite da nalog sa takvim nalogom nije kreiran na glavnom serveru
  • Napravi novi račun
  • Dodajte rezultate registracije u modul plaćanja i 1c broj u uslugu rezultata registracije
  • Dodajte informacije o računu ovoj tabeli
  • Kreirajte broj tačke za ovog klijenta u servisu poena. Proslijedite broj svog 1c računa ovoj usluzi.

A na ovoj listi je bilo još oko 10 poslovnih operacija sa užasnom vezom. Objekat naloga je bio potreban skoro svima. ID tačke i ime klijenta bili su potrebni u polovini poziva.

Nakon sat vremena refaktoriranja, uspjeli smo razdvojiti infrastrukturni kod i neke nijanse rada s računom u zasebne metode/klase. Metoda Boga je to olakšala, ali je ostalo 100 linija koda koje se jednostavno nisu željele raspetljati.

Tek nakon nekoliko dana postalo je jasno da je suština ove “lagane” metode poslovni algoritam. I da je originalni opis tehničkih specifikacija bio prilično složen. A pokušaj razbijanja ove metode na komade je ono što će narušiti SRP, a ne obrnuto.

Formalizam.

Vrijeme je da ostavimo našeg pijanca na miru. Osušite suze - sigurno ćemo se jednom vratiti. Sada da formaliziramo znanje iz ovog članka.

Formalizam 1. Definicija SRP

  1. Odvojite elemente tako da svaki od njih bude odgovoran za jednu stvar.
  2. Odgovornost predstavlja „razlog za promjenu“. Odnosno, svaki element ima samo jedan razlog za promjenu, u smislu poslovne logike.
  3. Potencijalne promjene poslovne logike. mora biti lokalizovan. Elementi koji se mijenjaju sinhrono moraju biti u blizini.

Formalizam 2. Neophodni kriterijumi samotestiranja.

Nisam vidio dovoljno kriterija za ispunjavanje SRP-a. Ali postoje neophodni uslovi:

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

objašnjenja

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

2) Ispravljanje greške ili dodavanje nove funkcije utiče na minimalni broj fajlova/klasa. Idealno - jedan.

objašnjenja

Pošto je odgovornost (za funkciju ili grešku) sadržana u jednom fajlu/klasi, tačno znate gde da tražite i šta da uredite. Na primjer: funkcija promjene izlaza operacija evidentiranja zahtijeva promjenu samo logera. Nema potrebe da prolazite kroz ostatak koda.

Drugi primjer je dodavanje nove UI kontrole, slične prethodnim. Ako vas to prisili da dodate 10 različitih entiteta i 15 različitih pretvarača, izgleda da ste pretjerali.

3) Ako nekoliko programera radi na različitim karakteristikama vašeg projekta, onda je vjerovatnoća sukoba spajanja, odnosno vjerovatnoća da će isti fajl/klasu promijeniti nekoliko programera u isto vrijeme, minimalna.

objašnjenja

Ako, dodajući novu operaciju „Ulijte votku ispod stola“, trebate utjecati na drvosječu, operaciju pijenja i točenja, onda izgleda da su odgovornosti krivo podijeljene. Naravno, to nije uvijek moguće, ali treba pokušati smanjiti ovu cifru.

4) Kada vam se postavi pitanje koje pojašnjava o poslovnoj logici (od programera ili menadžera), idete striktno u jednu klasu/fajl i primate informacije samo odatle.

objašnjenja

Karakteristike, pravila ili algoritmi su napisani kompaktno, svaki na jednom mjestu, a ne razbacani zastavicama po cijelom kodnom prostoru.

5) Imenovanje je jasno.

objašnjenja

Naša klasa ili metoda je odgovorna za jednu stvar, a odgovornost se ogleda u njenom imenu

AllManagersManagerService - najvjerovatnije klasa Boga
LocalPayment - vjerovatno ne

Formalizam 3. Occam-prva razvojna metodologija.

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 napraviti greške na različite načine:

  • Učinite objekte prevelikim spajanjem različitih odgovornosti
  • Preoblikovanje podjelom jedne odgovornosti na mnogo različitih tipova
  • Netačno definisane granice odgovornosti

Važno je zapamtiti pravilo: “bolje je napraviti veliku grešku” ili “ako niste sigurni, nemojte ga dijeliti”. Ako, na primjer, vaša klasa sadrži dvije odgovornosti, onda je i dalje razumljiva i može se podijeliti na dva uz minimalne promjene u klijentskom kodu. Sastavljanje čaše od krhotina stakla obično je teže zbog toga što je kontekst raspoređen na nekoliko datoteka i nedostatka potrebnih ovisnosti u kodu klijenta.

Vrijeme je da to nazovemo

Opseg SRP-a nije ograničen na OOP i SOLID. Primjenjuje se na metode, funkcije, klase, module, mikroservise i usluge. Primjenjuje se i na razvoj "figax-figax-and-prod" i na razvoj "raketne nauke", čineći svijet malo boljim svuda. Ako razmislite o tome, ovo je gotovo osnovni princip svakog inženjerstva. Mašinsko inženjerstvo, upravljački sistemi i zaista svi složeni sistemi su izgrađeni od komponenti, a „nedovoljna fragmentacija“ lišava dizajnere fleksibilnosti, „prekomerna fragmentacija“ lišava dizajnere efikasnosti, a pogrešne granice lišavaju ih razuma i mira.

Princip jedinstvene odgovornosti. Nije tako jednostavno kao što se čini

SRP nije izmišljen po prirodi i nije dio egzaktne nauke. Izbija iz naših bioloških i psiholoških ograničenja.To je samo način da se kontrolišu i razvijaju složeni sistemi pomoću mozga majmuna. On nam govori kako da dekomponujemo sistem. Originalna formulacija zahtijevala je priličnu količinu telepatije, ali nadam se da će ovaj članak očistiti dio dimne zavjese.

izvor: www.habr.com

Dodajte komentar