Enkelt ansvarsprincip. Ikke så simpelt som det ser ud til

Enkelt ansvarsprincip. Ikke så simpelt som det ser ud til Princippet om enkelt ansvar, også kendt som princippet om enkelt ansvar,
aka princippet om ensartet variabilitet - en ekstremt glat fyr at forstå og sådan et nervøst spørgsmål ved et programmørinterview.

Mit første seriøse bekendtskab med dette princip fandt sted i begyndelsen af ​​det første år, hvor de unge og grønne blev taget i skoven for at lave elever ud af larver - rigtige elever.

I skoven blev vi delt op i grupper på 8-9 personer hver og havde en konkurrence - hvilken gruppe der hurtigst ville drikke en flaske vodka, forudsat at den første person fra gruppen skænker vodka i et glas, den anden drikker den, og den tredje har en snack. Den enhed, der har afsluttet sin operation, flytter til slutningen af ​​gruppens kø.

Det tilfælde, hvor køstørrelsen var et multiplum af tre, var en god implementering af SRP.

Definition 1. Enkelt ansvar.

Den officielle definition af Single Responsibility Principle (SRP) siger, at hver enhed har sit eget ansvar og sin eksistensgrund, og den har kun ét ansvar.

Overvej objektet "Drikker" (Tippler).
For at implementere SRP-princippet vil vi opdele ansvarsområderne i tre:

  • Man hælder (Pour Operation)
  • En drink (DrinkUp Operation)
  • Man har en snack (TakeBiteOperation)

Hver af deltagerne i processen er ansvarlige for én komponent i processen, det vil sige, har ét atomansvar - at drikke, hælde eller snack.

Drikkehullet er til gengæld en facade til disse operationer:

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

Enkelt ansvarsprincip. Ikke så simpelt som det ser ud til

Hvorfor?

Den menneskelige programmør skriver kode til abemennesket, og abemennesket er uopmærksom, dum og har altid travlt. Han kan holde og forstå omkring 3 - 7 udtryk på én gang.
I tilfælde af en drukkenbolt er der tre af disse udtryk. Men hvis vi skriver koden med ét ark, så vil den indeholde hænder, briller, slagsmål og endeløse argumenter om politik. Og alt dette vil være i kroppen af ​​en metode. Jeg er sikker på, at du har set en sådan kode i din praksis. Ikke den mest humane test for psyken.

På den anden side er abemanden designet til at simulere objekter fra den virkelige verden i hans hoved. I sin fantasi kan han skubbe dem sammen, samle nye genstande fra dem og skille dem ad på samme måde. Forestil dig en gammel modelbil. I din fantasi kan du åbne døren, skrue dørbeklædningen af ​​og se vinduesløftemekanismerne, inden i hvilke der vil være gear. Men du kan ikke se alle maskinens komponenter på samme tid, i én "liste". Det kan "abemanden" i hvert fald ikke.

Derfor nedbryder menneskelige programmører komplekse mekanismer til et sæt af mindre komplekse og fungerende elementer. Det kan dog nedbrydes på forskellige måder: I mange gamle biler går luftkanalen ind i døren, og i moderne biler forhindrer en fejl i låseelektronikken motoren i at starte, hvilket kan være et problem under reparationer.

nu, SRP er et princip, der forklarer, HVORDAN man nedbrydes, det vil sige, hvor man trækker skillelinjen.

Han siger, at det er nødvendigt at nedbryde i henhold til princippet om opdeling af "ansvar", det vil sige i henhold til opgaver for bestemte objekter.

Enkelt ansvarsprincip. Ikke så simpelt som det ser ud til

Lad os vende tilbage til at drikke og de fordele, som abemanden får under nedbrydning:

  • Koden er blevet ekstremt klar på alle niveauer
  • Koden kan skrives af flere programmører på én gang (hver skriver et separat element)
  • Automatiseret test er forenklet - jo enklere elementet er, jo lettere er det at teste
  • Kodens sammensætning vises - du kan erstatte DrinkUp Operation til en operation, hvor en drukkenbolt hælder væske under bordet. Eller udskift hældeoperationen med en operation, hvor du blander vin og vand eller vodka og øl. Afhængigt af forretningskrav kan du gøre alt uden at røre ved metodekoden Tippler.Act.
  • Fra disse operationer kan du folde frådseren (kun ved hjælp af TakeBitOperation), Alkoholisk (kun ved brug af DrinkUp Operation direkte fra flasken) og opfylder mange andre forretningskrav.

(Åh, det ser ud til, at dette allerede er et OCP-princip, og jeg har overtrådt ansvaret for dette indlæg)

Og selvfølgelig ulemperne:

  • Vi bliver nødt til at skabe flere typer.
  • En drukkenbolt drikker for første gang et par timer senere, end han ellers ville have gjort.

Definition 2. Unified variabilitet.

Tillad mig, mine herrer! Drikkeklassen har også et enkelt ansvar – den drikker! Og generelt er ordet "ansvar" et ekstremt vagt begreb. Nogen er ansvarlig for menneskehedens skæbne, og nogen er ansvarlig for at rejse de pingviner, der blev væltet ved polen.

Lad os overveje to implementeringer af drikkeren. Den første, nævnt ovenfor, indeholder tre klasser - hælde, drikke og snack.

Den anden er skrevet gennem metoden "Forward and Only Forward" og indeholder al logikken i metoden Lov:

//Не тратьте время  на изучение этого класса. Лучше съешьте печеньку
с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 klasser, set fra en udefrakommende observatørs synspunkt, ser nøjagtig ens ud og deler det samme ansvar for at "drikke".

Forvirring!

Så går vi online og finder ud af en anden definition af SRP - Single Changeability Principle.

SCP oplyser, at "Et modul har én og kun én grund til at skifte". Det vil sige, "Ansvar er en grund til forandring."

(Det ser ud til, at de fyre, der kom med den oprindelige definition, var sikre på abemandens telepatiske evner)

Nu falder alt på plads. Separat kan vi ændre procedurerne for hælde-, drikke- og mellemmåltider, men i selve drikkeren kan vi kun ændre rækkefølgen og sammensætningen af ​​operationer, for eksempel ved at flytte snacken, før den drikkes eller tilføje læsning af en toast.

I "Forward and Only Forward" tilgangen ændres alt, hvad der kan ændres, kun i metoden Lov. Dette kan være læseligt og effektivt, når der er lidt logik, og det sjældent ændrer sig, men ofte ender det i forfærdelige metoder på 500 linjer hver, med flere hvis-udsagn end nødvendigt for, at Rusland kan tilslutte sig NATO.

Definition 3. Lokalisering af ændringer.

Drinkere forstår ofte ikke, hvorfor de vågnede i en andens lejlighed, eller hvor deres mobiltelefon er. Det er tid til at tilføje detaljeret logning.

Lad os begynde at logge med hældeprocessen:

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 at indkapsle det Pour Operation, vi handlede klogt ud fra et ansvars- og indkapslingssynspunkt, men nu er vi forvekslet med princippet om variabilitet. Udover selve operationen, som kan ændre sig, bliver selve logningen også foranderlig. Du bliver nødt til at adskille og oprette en speciel logger til hældeoperationen:

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

Det vil den omhyggelige læser bemærke LogEfter, LogFør и OnError kan også ændres individuelt og, analogt med de foregående trin, vil der oprettes tre klasser: PourLoggerFør, PourLoggerAfter и PourErrorLogger.

Og husker vi, at der er tre operationer for en drikker, får vi ni skovhugstklasser. Som følge heraf består hele drikkekredsen af ​​14 (!!!) klasser.

Hyperbel? Næsten! En abemand med en nedbrydningsgranat vil opdele "skænkeren" i en karaffel, et glas, skænkeoperatører, en vandforsyningstjeneste, en fysisk model af kollisionen af ​​molekyler, og i det næste kvartal vil han forsøge at løse afhængighederne uden globale variabler. Og tro mig, han vil ikke stoppe.

Det er på dette tidspunkt, at mange kommer til den konklusion, at SRP er eventyr fra lyserøde kongeriger, og går væk for at spille nudler...

... uden nogensinde at lære om eksistensen af ​​en tredje definition af Srp:

"Det fælles ansvarsprincip siger det ting, der ligner ændringer, skal opbevares ét sted". eller "Det, der ændrer sig sammen, skal opbevares ét sted"

Det vil sige, at hvis vi ændrer logningen af ​​en operation, så skal vi ændre den ét sted.

Dette er et meget vigtigt punkt - da alle forklaringerne af SRP, der var ovenfor, sagde, at det var nødvendigt at knuse typerne, mens de blev knust, det vil sige, de pålagde en "begrænsning ovenfra" på objektets størrelse, og nu taler vi om en "begrænsning nedefra" . Med andre ord, SRP kræver ikke kun "knusning under knusning", men også for ikke at overdrive det - "knus ikke sammenlåsende ting". Dette er den store kamp mellem Occams barbermaskine og abemanden!

Enkelt ansvarsprincip. Ikke så simpelt som det ser ud til

Nu skulle drikkeren have det bedre. Ud over det faktum, at der ikke er behov for at opdele IPourLogger-loggeren i tre klasser, kan vi også kombinere alle loggere i é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 tilføjer en fjerde type operation, så er logningen for den allerede klar. Og koden for selve operationerne er ren og fri for infrastrukturstøj.

Som et resultat har vi 5 klasser til at løse drikkeproblemet:

  • Skænkeoperation
  • Drikkeoperation
  • Jamming operation
  • Logger
  • Drinker facade

Hver af dem er strengt ansvarlige for én funktionalitet og har én grund til ændring. Alle regler, der ligner ændringer, er placeret i nærheden.

Eksempel fra det virkelige liv

Vi skrev engang en service til automatisk registrering af en b2b-klient. Og en GUD-metode dukkede op for 200 linjer med lignende indhold:

  • Gå til 1C og opret en konto
  • Med denne konto skal du gå til betalingsmodulet og oprette det der
  • Tjek, at en konto med en sådan konto ikke er oprettet på hovedserveren
  • Oprette en ny konto
  • Tilføj registreringsresultaterne i betalingsmodulet og 1c-nummeret til registreringsresultattjenesten
  • Tilføj kontooplysninger til denne tabel
  • Opret et punktnummer for denne klient i punkttjenesten. Giv dit 1c-kontonummer til denne tjeneste.

Og der var omkring 10 flere forretningsaktiviteter på denne liste med forfærdelige tilslutningsmuligheder. Næsten alle havde brug for kontoobjektet. Punkt-id'et og klientnavnet var nødvendigt i halvdelen af ​​opkaldene.

Efter en times refactoring var vi i stand til at adskille infrastrukturkoden og nogle af nuancerne ved at arbejde med en konto i separate metoder/klasser. Gud-metoden gjorde det nemmere, men der var 100 linjer kode tilbage, som bare ikke ønskede at blive viklet ud.

Først efter et par dage blev det klart, at essensen af ​​denne "lette" metode er en forretningsalgoritme. Og at den oprindelige beskrivelse af de tekniske specifikationer var ret kompleks. Og det er forsøget på at bryde denne metode i stykker, der vil overtræde SRP, og ikke omvendt.

Formalisme.

Det er tid til at lade vores fulde være i fred. Tør dine tårer - vi vender helt sikkert tilbage til det en dag. Lad os nu formalisere viden fra denne artikel.

Formalisme 1. Definition af SRP

  1. Adskil elementerne, så hver af dem er ansvarlige for én ting.
  2. Ansvar står for "grund til at ændre". Det vil sige, at hvert element kun har én grund til forandring, i form af forretningslogik.
  3. Potentielle ændringer i forretningslogikken. skal lokaliseres. Elementer, der ændrer sig synkront, skal være i nærheden.

Formalisme 2. Nødvendige selvtestkriterier.

Jeg har ikke set tilstrækkelige kriterier for at opfylde SRP. Men der er nødvendige betingelser:

1) Spørg dig selv, hvad denne klasse/metode/modul/tjeneste gør. du skal besvare det med en simpel definition. ( Tak skal du have Brightori )

forklaringer

Men nogle gange er det meget svært at finde en simpel definition

2) At rette en fejl eller tilføje en ny funktion påvirker et minimum antal filer/klasser. Ideelt set - en.

forklaringer

Da ansvaret (for en funktion eller fejl) er indkapslet i én fil/klasse, ved du præcis, hvor du skal kigge, og hvad du skal redigere. For eksempel: funktionen til at ændre outputtet af logningsoperationer kræver kun at ændre loggeren. Der er ingen grund til at køre gennem resten af ​​koden.

Et andet eksempel er at tilføje en ny UI-kontrol, der ligner de tidligere. Hvis dette tvinger dig til at tilføje 10 forskellige entiteter og 15 forskellige konvertere, ser det ud til, at du overdriver det.

3) Hvis flere udviklere arbejder på forskellige funktioner i dit projekt, så er sandsynligheden for en flettekonflikt, det vil sige sandsynligheden for, at den samme fil/klasse ændres af flere udviklere på samme tid, minimal.

forklaringer

Hvis du, når du tilføjer en ny operation "Hæld vodka under bordet", skal påvirke loggeren, driften af ​​at drikke og hælde, så ser det ud til, at ansvaret er skævt fordelt. Det er selvfølgelig ikke altid muligt, men vi bør forsøge at reducere dette tal.

4) Når du bliver stillet et opklarende spørgsmål om forretningslogik (fra en udvikler eller leder), går du strengt ind i én klasse/fil og modtager kun information derfra.

forklaringer

Funktioner, regler eller algoritmer er skrevet kompakt, hver på ét sted og ikke spredt med flag i hele koderummet.

5) Navngivningen er klar.

forklaringer

Vores klasse eller metode er ansvarlig for én ting, og ansvaret afspejles i dens navn

AllManagersManagerService - højst sandsynligt en Gud klasse
LocalPayment - sandsynligvis ikke

Formalisme 3. Occam-første udviklingsmetodologi.

I begyndelsen af ​​designet kender abemanden ikke og føler ikke alle finesser af problemet løst og kan lave en fejl. Du kan lave fejl på forskellige måder:

  • Gør objekter for store ved at flette forskellige ansvarsområder
  • Reframing ved at opdele et enkelt ansvar i mange forskellige typer
  • Forkert at definere ansvarsgrænserne

Det er vigtigt at huske reglen: "det er bedre at lave en stor fejl", eller "hvis du ikke er sikker, så lad være med at dele den op." Hvis din klasse for eksempel indeholder to ansvarsområder, så er det stadig forståeligt og kan opdeles i to med minimale ændringer i klientkoden. Samling af et glas fra glasskår er normalt vanskeligere på grund af, at konteksten er spredt på flere filer og manglen på nødvendige afhængigheder i klientkoden.

Det er tid til at kalde det en dag

Omfanget af SRP er ikke begrænset til OOP og SOLID. Det gælder for metoder, funktioner, klasser, moduler, mikrotjenester og tjenester. Det gælder både "figax-figax-and-prod" og "rocket-science" udvikling, hvilket gør verden lidt bedre overalt. Hvis du tænker over det, er dette næsten det grundlæggende princip for al teknik. Maskinteknik, kontrolsystemer og faktisk alle komplekse systemer er bygget af komponenter, og "underfragmentering" fratager designere fleksibilitet, "overfragmentering" fratager designere effektivitet, og forkerte grænser fratager dem fornuft og ro i sindet.

Enkelt ansvarsprincip. Ikke så simpelt som det ser ud til

SRP er ikke opfundet af naturen og er ikke en del af eksakt videnskab. Det bryder ud af vores biologiske og psykologiske begrænsninger.Det er blot en måde at kontrollere og udvikle komplekse systemer på ved hjælp af abe-menneskets hjerne. Han fortæller os, hvordan man nedbryder et system. Den originale formulering krævede en del telepati, men jeg håber, at denne artikel rydder noget af røgslørret.

Kilde: www.habr.com

Tilføj en kommentar