Enkelt ansvarsprinsipp. Ikke så enkelt som det virker

Enkelt ansvarsprinsipp. Ikke så enkelt som det virker Enkeltansvarsprinsipp, også kjent som prinsippet om enkeltansvar,
aka prinsippet om enhetlig variasjon - en ekstremt glatt fyr å forstå og et så nervøst spørsmål ved et programmererintervju.

Mitt første seriøse bekjentskap med dette prinsippet fant sted i begynnelsen av mitt første år, da de unge og grønne ble tatt til skogs for å lage ekte studenter av larver.

I skogen ble vi delt inn i grupper på 8-9 personer hver og hadde en konkurranse - hvilken gruppe som ville drikke en flaske vodka raskest, forutsatt at den første personen fra gruppen heller vodka i et glass, den andre drikker den, og den tredje har en matbit. Enheten som har fullført sin operasjon flytter til slutten av gruppens kø.

Tilfellet der køstørrelsen var et multiplum av tre var en god implementering av SRP.

Definisjon 1. Enkeltansvar.

Den offisielle definisjonen av Single Responsibility Principle (SRP) sier at hver enhet har sitt eget ansvar og eksistensgrunn, og den har bare ett ansvar.

Tenk på objektet "Drikker" (Tippler).
For å implementere SRP-prinsippet vil vi dele ansvaret inn i tre:

  • En heller (Helledrift)
  • En drikker (DrikkeOperasjon)
  • Man har en matbit (TakeBiteOperation)

Hver av deltakerne i prosessen er ansvarlig for en komponent i prosessen, det vil si har ett atomansvar - å drikke, helle eller snacks.

Drikkehullet er på sin side en fasade for disse operasjonene:

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

Enkelt ansvarsprinsipp. Ikke så enkelt som det virker

Hvorfor?

Den menneskelige programmereren skriver kode for apemennesket, og apemennesket er uoppmerksom, dum og har det alltid travelt. Han kan holde og forstå omtrent 3 - 7 termer på en gang.
Når det gjelder en fylliker, er det tre av disse begrepene. Men hvis vi skriver koden med ett ark, vil den inneholde hender, briller, slåsskamper og uendelige argumenter om politikk. Og alt dette vil være i kroppen til en metode. Jeg er sikker på at du har sett slik kode i praksisen din. Ikke den mest humane testen for psyken.

På den annen side er apemennesket designet for å simulere virkelige objekter i hodet hans. I fantasien kan han skyve dem sammen, sette sammen nye gjenstander fra dem og demontere dem på samme måte. Se for deg en gammel bilmodell. I fantasien din kan du åpne døren, skru av dørlisten og se vindusløftmekanismene der det vil være gir. Men du kan ikke se alle komponentene til maskinen samtidig, i én «oppføring». Det kan i hvert fall ikke "apemannen".

Derfor dekomponerer menneskelige programmerere komplekse mekanismer til et sett med mindre komplekse og fungerende elementer. Det kan imidlertid dekomponeres på forskjellige måter: i mange gamle biler går luftkanalen inn i døren, og i moderne biler hindrer en svikt i låselektronikken at motoren starter, noe som kan være et problem under reparasjoner.

SRP er et prinsipp som forklarer HVORDAN man skal dekomponere, det vil si hvor man skal trekke skillelinjen.

Han sier at det er nødvendig å dekomponere i henhold til prinsippet om deling av "ansvar", det vil si i henhold til oppgavene til visse objekter.

Enkelt ansvarsprinsipp. Ikke så enkelt som det virker

La oss gå tilbake til drikking og fordelene som apemannen får under nedbrytning:

  • Koden har blitt ekstremt tydelig på alle nivåer
  • Koden kan skrives av flere programmerere samtidig (hver skriver et eget element)
  • Automatisert testing er forenklet – jo enklere elementet er, desto lettere er det å teste
  • Komposisjonen til koden vises - du kan erstatte DrikkeOperasjon til en operasjon der en fylliker heller væske under bordet. Eller bytt ut skjenkingen med en operasjon der du blander vin og vann eller vodka og øl. Avhengig av forretningskrav kan du gjøre alt uten å berøre metodekoden Tippler.Act.
  • Fra disse operasjonene kan du brette frosseren (kun ved å bruke TakeBitOperation), Alkoholholdig (bruker kun DrikkeOperasjon rett fra flasken) og oppfyller mange andre forretningskrav.

(Å, det ser ut til at dette allerede er et OCP-prinsipp, og jeg har brutt ansvaret for dette innlegget)

Og selvfølgelig ulempene:

  • Vi må lage flere typer.
  • En fylliker drikker for første gang et par timer senere enn han ellers ville ha gjort.

Definisjon 2. Unified variabilitet.

Tillat meg, mine herrer! Drikkeklassen har også et enkelt ansvar – den drikker! Og generelt er ordet "ansvar" et ekstremt vagt begrep. Noen er ansvarlige for menneskehetens skjebne, og noen er ansvarlig for å heve pingvinene som ble veltet ved polet.

La oss vurdere to implementeringer av drikkeren. Den første, nevnt ovenfor, inneholder tre klasser - helle, drikke og snacks.

Den andre er skrevet gjennom "Forward and Only Forward"-metoden og inneholder all logikken i metoden Handling:

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

Begge disse klassene, fra synspunktet til en utenforstående observatør, ser nøyaktig like ut og deler det samme ansvaret for å "drikke".

Forvirring!

Så går vi online og finner ut en annen definisjon av SRP - Single Changeability Principle.

SCP uttaler at "En modul har én og bare én grunn til å endre". Det vil si: "Ansvar er en grunn til endring."

(Det ser ut til at gutta som kom opp med den opprinnelige definisjonen var sikre på de telepatiske evnene til apemannen)

Nå faller alt på plass. Hver for seg kan vi endre prosedyrene for skjenking, drikking og småspising, men i selve drikkeren kan vi bare endre rekkefølgen og sammensetningen av operasjoner, for eksempel ved å flytte snacken før vi drikker eller legge til avlesningen av en toast.

I «Forward and Only Forward»-tilnærmingen endres alt som kan endres kun i metoden Handling. Dette kan være lesbart og effektivt når det er lite logikk og det sjelden endres, men ofte ender det opp i forferdelige metoder på 500 linjer hver, med flere hvis-uttalelser enn det som kreves for at Russland skal bli med i NATO.

Definisjon 3. Lokalisering av endringer.

Drikkere forstår ofte ikke hvorfor de våknet i en annens leilighet, eller hvor mobiltelefonen deres er. Det er på tide å legge til detaljert logging.

La oss begynne å logge med skjenkeprosessen:

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

Ved å kapsle det inn Helledrift, vi handlet klokt fra et ansvars- og innkapslingssynspunkt, men nå er vi forvekslet med variabilitetsprinsippet. I tillegg til selve operasjonen, som kan endres, blir selve loggingen også foranderlig. Du må skille og lage en spesiell logger for helleoperasjonen:

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

Den nitid leser vil merke det Logg Etter, Loggfør и OnError kan også endres individuelt, og vil, analogt med de foregående trinnene, opprette tre klasser: PourLoggerFør, PourLoggerEtter и PourErrorLogger.

Og husker vi at det er tre operasjoner for en drikker, får vi ni hogstklasser. Som et resultat består hele drikkesirkelen av 14 (!!!) klasser.

Hyperbel? Neppe! En apemann med en nedbrytningsgranat vil dele opp "hellet" i en karaffel, et glass, skjenkeoperatører, en vannforsyningstjeneste, en fysisk modell av kollisjonen av molekyler, og i neste kvartal vil han prøve å løse avhengighetene uten globale variabler. Og tro meg, han vil ikke stoppe.

Det er på dette tidspunktet mange kommer til den konklusjonen at SRP er eventyr fra rosa riker, og drar bort for å spille nudler...

... uten noen gang å lære om eksistensen av en tredje definisjon av Srp:

«Sentansvarsprinsippet sier det ting som ligner endring bør lagres på ett sted". eller "Det som endres sammen bør holdes på ett sted"

Det vil si at hvis vi endrer loggingen av en operasjon, så må vi endre den på ett sted.

Dette er et veldig viktig poeng - siden alle forklaringene til SRP som var ovenfor sa at det var nødvendig å knuse typene mens de ble knust, det vil si at de påla en "øvre grense" på størrelsen på objektet, og nå vi snakker allerede om en "nedre grense" . Med andre ord, SRP krever ikke bare "knusing mens du knuser", men også å ikke overdrive det - "ikke knus sammenlåsende ting". Dette er den store kampen mellom Occams barberhøvel og apemannen!

Enkelt ansvarsprinsipp. Ikke så enkelt som det virker

Nå skal den som drikker føle seg bedre. I tillegg til at det ikke er behov for å dele IPourLogger-loggeren i tre klasser, kan vi også kombinere alle loggere til én type:

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

Og hvis vi legger til en fjerde type operasjon, er loggingen for den allerede klar. Og koden for selve operasjonene er ren og fri for støy fra infrastruktur.

Som et resultat har vi 5 klasser for å løse drikkeproblemet:

  • Helleoperasjon
  • Drikkeoperasjon
  • Jamming operasjon
  • Logger
  • Drinker fasade

Hver av dem er strengt ansvarlig for én funksjonalitet og har én grunn til endring. Alle regler som ligner på endringer er plassert i nærheten.

Eksempel fra det virkelige liv

Vi skrev en gang en tjeneste for automatisk registrering av en b2b-klient. Og en GUD-metode dukket opp for 200 linjer med lignende innhold:

  • Gå til 1C og opprett en konto
  • Med denne kontoen går du til betalingsmodulen og oppretter den der
  • Sjekk at en konto med en slik konto ikke er opprettet på hovedserveren
  • Lag en ny bruker
  • Legg til registreringsresultatene i betalingsmodulen og 1c-nummeret til registreringsresultattjenesten
  • Legg til kontoinformasjon i denne tabellen
  • Opprett et poengnummer for denne klienten i punkttjenesten. Send 1c-kontonummeret ditt til denne tjenesten.

Og det var omtrent 10 flere forretningsoperasjoner på denne listen med forferdelig tilkobling. Nesten alle trengte kontoobjektet. Punkt-ID og klientnavn var nødvendig i halvparten av samtalene.

Etter en times refaktorering klarte vi å dele infrastrukturkoden og noen av nyansene ved å jobbe med en konto i separate metoder/klasser. Gud-metoden gjorde det lettere, men det var 100 linjer med kode igjen som bare ikke ønsket å bli løst.

Først etter noen dager ble det klart at essensen av denne "lette" metoden er en forretningsalgoritme. Og at den opprinnelige beskrivelsen av de tekniske spesifikasjonene var ganske kompleks. Og det er forsøket på å bryte denne metoden i biter som vil bryte SRP, og ikke omvendt.

Formalisme.

Det er på tide å la vår fulle være i fred. Tørk tårene dine – vi kommer garantert tilbake til det en dag. La oss nå formalisere kunnskapen fra denne artikkelen.

Formalisme 1. Definisjon av SRP

  1. Skille elementene slik at hver av dem er ansvarlig for én ting.
  2. Ansvar står for "grunn til å endre". Det vil si at hvert element kun har én grunn til endring, når det gjelder forretningslogikk.
  3. Potensielle endringer i forretningslogikk. må lokaliseres. Elementer som endres synkront må være i nærheten.

Formalisme 2. Nødvendige selvtestkriterier.

Jeg har ikke sett tilstrekkelige kriterier for å oppfylle SRP. Men det er nødvendige forhold:

1) Spør deg selv hva denne klassen/metoden/modulen/tjenesten gjør. du må svare på det med en enkel definisjon. ( Takk skal du ha Brightori )

forklaringer

Noen ganger er det imidlertid veldig vanskelig å finne en enkel definisjon

2) Å fikse en feil eller legge til en ny funksjon påvirker et minimum antall filer/klasser. Ideelt sett - en.

forklaringer

Siden ansvaret (for en funksjon eller feil) er innkapslet i én fil/klasse, vet du nøyaktig hvor du skal lete og hva du skal redigere. For eksempel: funksjonen for å endre utdata for loggingsoperasjoner vil kreve at du bare endrer loggeren. Det er ikke nødvendig å kjøre gjennom resten av koden.

Et annet eksempel er å legge til en ny UI-kontroll, lik de forrige. Hvis dette tvinger deg til å legge til 10 forskjellige enheter og 15 forskjellige omformere, ser det ut til at du overdriver det.

3) Hvis flere utviklere jobber med ulike funksjoner i prosjektet ditt, så er sannsynligheten for en sammenslåingskonflikt, det vil si sannsynligheten for at samme fil/klasse endres av flere utviklere samtidig, minimal.

forklaringer

Hvis du, når du legger til en ny operasjon "Hell vodka under bordet", må påvirke loggeren, driften av å drikke og skjenke, ser det ut til at ansvaret er skjevt delt. Dette er selvfølgelig ikke alltid mulig, men vi bør prøve å redusere dette tallet.

4) Når du får et oppklarende spørsmål om forretningslogikk (fra en utvikler eller leder), går du strengt tatt inn i én klasse/fil og mottar kun informasjon derfra.

forklaringer

Funksjoner, regler eller algoritmer er skrevet kompakt, hver på ett sted, og ikke spredt med flagg i hele koderommet.

5) Navnet er klart.

forklaringer

Vår klasse eller metode er ansvarlig for én ting, og ansvaret gjenspeiles i navnet

AllManagersManagerService - mest sannsynlig en gudsklasse
LocalPayment - sannsynligvis ikke

Formalisme 3. Occam-første utviklingsmetodikk.

I begynnelsen av designen vet ikke apemannen og føler ikke at alle finessene i problemet blir løst og kan gjøre en feil. Du kan gjøre feil på forskjellige måter:

  • Gjør objekter for store ved å slå sammen ulike ansvarsområder
  • Reframing ved å dele et enkelt ansvar i mange forskjellige typer
  • Feil definer ansvarsgrensene

Det er viktig å huske regelen: "det er bedre å gjøre en stor feil," eller "hvis du ikke er sikker, ikke del den opp." Hvis for eksempel klassen din inneholder to ansvarsområder, er den fortsatt forståelig og kan deles i to med minimale endringer i klientkoden. Å sette sammen et glass fra glasskår er vanligvis vanskeligere på grunn av at konteksten er spredt på flere filer og mangel på nødvendige avhengigheter i klientkoden.

Det er på tide å kalle det en dag

Omfanget av SRP er ikke begrenset til OOP og SOLID. Det gjelder metoder, funksjoner, klasser, moduler, mikrotjenester og tjenester. Det gjelder både "figax-figax-and-prod" og "rocket-science" utvikling, noe som gjør verden litt bedre overalt. Hvis du tenker på det, er dette nesten det grunnleggende prinsippet for all ingeniørkunst. Mekanikk, kontrollsystemer og faktisk alle komplekse systemer er bygget av komponenter, og "underfragmentering" fratar designere fleksibilitet, "overfragmentering" frarøver konstruktører effektivitet, og feilaktige grenser fratar dem fornuft og sinnsro.

Enkelt ansvarsprinsipp. Ikke så enkelt som det virker

SRP er ikke oppfunnet av naturen og er ikke en del av eksakt vitenskap. Det bryter ut av våre biologiske og psykologiske begrensninger.Det er bare en måte å kontrollere og utvikle komplekse systemer ved å bruke apemenneskets hjerne. Han forteller oss hvordan vi skal dekomponere et system. Den opprinnelige formuleringen krevde en god del telepati, men jeg håper denne artikkelen fjerner noe av røykskjermen.

Kilde: www.habr.com

Legg til en kommentar