Princippet om enkelt ansvar, også kendt som princip om enkelt ansvar,
det samme, som princip om enkelt forandringsdygtighed — en yderst glat og kompleks koncept, der ofte er et nervepirrende emne i en programmerers jobsamtale.
Min første seriøse forbindelse til dette princip skete i starten af mit første semester, da vi, unge og usikre, blev taget med ud i skoven for at gøre os fra studenterlarver til rigtige studerende.
I skoven blev vi delt op i grupper af 8-9 personer hver, og der blev afholdt en konkurrence — hvilken gruppe kunne hurtigst drikke en flaske vodka under forudsætning af, at den første person hælder vodka i et glas, den anden drikker, og den tredje spiser noget. Den, der har udført sin opgave, stiller sig bagerst i køen.
En situation, hvor størrelsen af køen var et multiplum af tre, var en god implementering af SRP.
Definition 1. Enkelt ansvar.
Den officielle definition af princippet om enkelt ansvar (SRP) siger, at hver objekt har sit eget ansvar og eksistensberettigelse, og det ansvar er kun én.
Lad os overveje objektet «Tippler» (Tippler).
For at overholde princippet om SRP skal vi dele opgaverne mellem tre:
- En hælder (PourOperation)
- En drikker (DrinkUpOperation)
- En spiser (TakeBiteOperation)
Hver deltager i processen er ansvarlig for én komponent i processen, det vil sige har ét atomar ansvar — at drikke, hælde eller spise.
Tippler er i sin tur en facade for disse operationer:
class Tippler {
//...
void Act(){
_pourOperation.Do() // hælde
_drinkUpOperation.Do() // drikke
_takeBiteOperation.Do() // spise
}
}
Hvorfor?
Mennesket-programmør skriver kode til mennesket-abe, og mennesket-abe er uopmærksom, dum og altid i hast. Han kan holde og forstå omkring 3 til 7 termer ad gangen.
I tilfælde af Tippler er der tre termer. Dog, hvis vi skriver koden i én lang række, vil der komme hænder, glas, slagsmål og uendelige diskussioner om politik. Alt dette vil være inde i én metode. Jeg er sikker på, at du har set sådan kode i din praksis. Det er ikke den mest humane prøvelse for psyken.
På den anden side er menneske-aber designet til at modellere objekter fra den virkelige verden i deres sind. I deres fantasi kan de kollidere dem, samle nye objekter af dem og også demontere dem. Forestil dig en gammel bilmodel. Du kan i fantasi åbne døren, skrue dørens beklædning af og se derinde mekanismerne for vindueshævere, hvor der vil være gear. Men du kan ikke se alle bilens komponenter på én gang, i én "liste". I det mindste kan "menneske-aberen" ikke.
Derfor dekomponerer programmørmennesker komplekse mekanismer i et sæt mindre komplekse og funktionerende elementer. Men dekomponering kan gøres på forskellige måder: i mange gamle biler – går luftkanalen ind i døren, mens i moderne biler en fejl i låseelektronikken forhindrer motoren i at starte, hvilket gør reparationen vanskelig.
Så SRP er et princip, der forklarer HVORDAN man dekomponerer, det vil sige, hvor man skal drage skillelinjen..
Det siger, at man skal dekomponere efter princippet om "ansvarsopdeling", det vil sige efter opgaverne for forskellige objekter.
Lad os vende tilbage til drikeren og de fordele, som menneske-aberen får ved dekomponering:
- Koden bliver fuldstændig klar på hvert niveau.
- Koden kan skrives af flere programmører samtidig (hver skriver et separat element).
- Automatisk testning forenkles – jo enklere elementet er, desto lettere er det at teste det.
- Der opstår komposition af kode – man kan erstatte DrinkUpOperation med en operation, hvor drikeren hælder væsken ud under bordet. Eller erstatte hældningen med en operation, hvor man blander vin og vand eller vodka og øl. Afhængigt af forretningskravene kan man gøre alt dette uden at røre ved metoden. Tippler.Act.
- Ud af disse operationer kan du sammensætte en gumse (ved kun at bruge TakeBitOperation), en alkoholiker (ved kun at bruge DrinkUpOperation direkte fra flasken) og opfylde mange andre forretningskrav.
(Åh, det ser ud til, at dette allerede er OCP-princippet, og jeg har krænket ansvaret for dette indlæg.)
Og selvfølgelig ulemperne:
- Man bliver nødt til at oprette flere typer.
- Drikeren vil drikke et par timer senere end han kunne have.
Definition 2. Enhedlig foranderlighed.
Lad os være enige! Klassedelen af drikeren har også det ene ansvar — at drikke! Og overhovedet er ordet "ansvar" et yderst vag koncept. Nogen er ansvarlig for menneskehedens skæbne, mens andre er ansvarlige for at løfte omkullkastede pingviner på polen.
Lad os se på to implementeringer af drikeren. Den første, nævnt ovenfor, indeholder tre klasser — hæld, drik og snack.
Den anden er skrevet efter metodologien "Fremad og kun fremad" og indeholder al logik i metoden 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);
}
}
}Begge disse klasser ser fra en udefrakommende observatørs synspunkt helt ens ud og udfører det samme ansvar "at drikke".
Pinligt!
Så graver vi i internettet og opdager en anden definition af SRP — Det enkle ændringsprincip (Single Changeability Principle).
SCP siger, at "Modulet har kun én og kun én grund til at ændre sig". Det vil sige, "Ansvar er en grund til at ændre sig".
(Det lader til, at drengene, der i første omgang opfandt definitionen, var sikre på menneske-abens telepatiske evner)
Nu falder alt på plads. Separat kan vi ændre procedurerne for hældning, drikning og snacks, og i selve drikeren kan vi kun ændre rækkefølgen og indholdet af operationerne, for eksempel ved at flytte snacks før drikning eller tilføje læsning af en skål.
I tilgangen "Fremad og kun fremad" ændres alt, hvad der kan ændres, kun i metoden Act. Dette kan være læsevenligt og effektivt i tilfælde, hvor logikken er sparsom og sjældent ændres, men ofte ender det med forfærdelige metoder på 500 linjer hver med et antal if-er større end nødvendigt for Ruslands optagelse i NATO.
Definition 3. Lokalisering af ændringer.
Drikere forstår ofte ikke, hvorfor de vågner op i en fremmed lejlighed, eller hvor deres mobiltelefon er. Det er tid til at tilføje detaljeret logning.
Lad os starte logningen med hældningsprocessen:
class PourOperation: IOperation{
PourOperation(ILogger log /*....*/){/*...*/}
//...
void Do(){
_log.Log($"Før hældning med {_hand} og {_bottle}");
//Hæld forretningslogik ...
_log.Log($"Efter hældning med {_hand} og {_bottle}");
}
}Ved at kapsle det ind i PourOperation, vi handlede klogt med hensyn til ansvar og indkapsling, men nu har vi et dilemma med hensyn til ændrede krav. Udover selve operationen, som kan ændre sig, bliver logningen også variabel. Vi må adskille og lave en specialiseret logger til hældningsoperationen:
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{
//... forretningslogik
_log.LogAfter(_hand, _bottle");
}
catch(exception e){
_log.OnError(_hand, _bottle, e)
}
}
}Den nysgerrige læser vil bemærke, at LogAfter, LogBefore og OnError kan også ændre sig individuelt, og ligesom med tidligere handlinger vil det skabe tre klasser: PourLoggerBefore, PourLoggerAfter og PourErrorLogger.
Og hvis vi husker, at der er tre operationer for hælderen — får vi ni loggingsklasser. I alt består hælderen af 14 (!!!) klasser.
Hyperbel? Næppe! Mennesket-aber med en dekompositionsgranat vil smuldre 'hælderen' ned i karaflen, glas, hældningsoperatører, vandservices, den fysiske model for molekylekollisioner, og næste kvartal vil forsøge at rede afhængighederne uden globale variable. Og tro mig — han stopper ikke.
Netop på dette tidspunkt konkluderer mange, at SRP er eventyr fra lyserøde kongedømmer og ender med at købe ind på det...
… uden nogensinde at lære om eksistensen af den tredje definition af SRP:
«Enhedsansvarsprincippet siger, at relaterede ting, der skal ændres, bør opbevares ét sted«. eller “Det, der ændrer sig sammen, skal opbevares ét sted.”
Det vil sige, hvis vi ændrer logningen af operationen, så skal vi ændre det ét sted.
Dette er et meget vigtigt punkt — da alle de tidligere forklaringer på SRP talte om at opdele typer, indtil de ikke kunne opdeles mere, lagde det et 'øvre begrænsning' på størrelsen af objektet, og nu taler vi også om et 'nederste begrænsning'. Med andre ord, SRP kræver ikke kun, at vi 'opdeler mens vi kan', men også at vi ikke går for vidt — 'ikke opdele sammenkædede ting'. Dette er den store kamp mellem Ockhams barberblad og mennesket-aber!
Nu burde det blive lettere for hælderen. Udover at vi ikke behøver at opdele loggeren IPourLogger i tre klasser, kan vi også samle 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 allerede klar til den. Og koden for selve operationerne er ren og fri for infrastrukturs støj.
Som et resultat har vi 5 klasser til at løse problemet med at hælde:
- Hældningsoperation
- Drikningsoperation
- Spisningoperation
- Logger
- Facade for druk
Hver af dem har et strengt ansvar for en funktionalitet og har kun én årsag til at ændre sig. Alle lignende ændringsregler ligger tæt sammen.
Eksempel fra det virkelige liv
Engang skrev vi en tjeneste til automatisk registrering af b2b-kunder. Og der opstod en GOD-metode på 200 linjer med lignende indhold:
- Gå til 1C og opret en konto
- Med denne konto, gå til betalingsmodulet og opret den der
- Tjek at en konto med denne nummer ikke er oprettet i hovedsystemet serveren
- Opret en ny konto
- Resultatet af registreringen i betalingsmodulet og nummeret fra 1C tilføj til registreringsresultatstjenesten
- Tilføj oplysninger om kontoen til denne tabel
- Opret et punktnummer for denne kunde i tjenesten for punkter. Overfør kontonummeret fra 1C til denne tjeneste.
Og der var i denne liste omkring 10 andre forretningsoperationer med forfærdelig sammenhæng. Kontoobjektet var nødvendigt for næsten alle. Punktidentifikatoren og kundens navn var påkrævet i halvdelen af opkaldene.
Efter en times refaktorering kunne vi adskille infrastrukturkode og nogle nuancer vedrørende arbejdet med kontoen i separate metoder/klasser. Godmetoden blev lettere, men der var stadig 100 linjer kode, der ikke ville trække sig tilbage.
Det var først efter et par dage, at det blev klart, at essensen af denne "lette" metode var forretningsalgoritmen. Og at den oprindelige beskrivelse af specifikationen var ret kompleks. Og at forsøget på at opdele denne metode ville overtræde SRP, ikke omvendt.
Formalismen.
Det er tid til at give vores drukker en pause. Tør tårerne — vi vil helt sikkert vende tilbage til ham en dag. Men nu skal vi formaliserer viden fra denne artikel.
Formalismen 1. Definition af SRP
- Opdel elementer således, at hver enkelt har ansvar for noget enkelt.
- Ansvar oversættes til "årsag til ændring". Det vil sige, at hvert element kun har én grund til at ændre sig, i termer af forretningslogik.
- Potentielle ændringer i forretningslogikken skal lokaliseres. Ændrede synkroniserede elementer skal være tæt på hinanden.
Formalisme 2. Nødvendige kriterier for selvkontrol.
Jeg har ikke set tilstrækkelige kriterier for opfyldelse af SRP. Men der er nødvendige betingelser:
1) Stil dig selv spørgsmålet – hvad gør denne klasse/metode/modul/tjeneste. Du skal kunne besvare det med en enkel definition. (tak) )
forklaringer
Det kan dog nogle gange være meget svært at finde en enkel definition.
2) Fiksering af en bestemt fejl eller tilføjelse af en ny funktion berører et minimum af filer/klasser. Ideelt set – én.
forklaringer
Da ansvaret (for funktionen eller fejlen) er indkapslet i én fil/klasse, ved du præcist, hvor du skal lede og hvad du skal rette. For eksempel: en funktion til at ændre logningen af operationer kræver kun ændring af loggertjenesten. Det er ikke nødvendigt at søge i den øvrige kode.
Et andet eksempel – tilføjelse af en ny UI-kontrol, der ligner de tidligere. Hvis det får dig til at tilføje 10 forskellige enheder og 15 forskellige konvertere – ser det ud til, at du er 'overkompliceret'.
3) Hvis flere udviklere arbejder på forskellige funktioner i dit projekt, er sandsynligheden for merge-konflikter, altså sandsynligheden for, at den samme fil/klasse ændres af flere udviklere samtidig – minimal.
forklaringer
Hvis du ved tilføjelse af en ny operation 'Hælde vodka under bordet' skal berøre loggertjenesten, operationen for at drikke og hælde – ser det ud til, at ansvaret er dårligt fordelt. Det er naturligvis ikke altid muligt, men man må forsøge at reducere denne indikator.
4) Ved et præciserende spørgsmål om forretningslogik (fra udvikleren eller lederen) dykker du strengt ned i én klasse/fil og får kun information derfra.
forklaringer
Funktioner, regler eller algoritmer er kompakt skrevet, hver på ét sted, og ikke spredt som flag over hele kodebasen.
5) Navngivning er forståelig.
forklaringer
Vores klasse eller metode er ansvarlig for én ting, og ansvaret afspejles i dens navn.
AllManagersManagerService – sandsynligvis en God-klasse.
LocalPayment – sandsynligvis ikke.
Formalisme 3. Udviklingsmetoden 'Ockhams Razor'.
I begyndelsen af designet ved eller føler menneske-aberne ikke alle nuancerne i den stillede opgave og kan lave fejl. Man kan fejle på forskellig vis:
- At skabe for store objekter ved at kombinere forskellige ansvarsområder
- At overfragmentere ved at dele et enkelt ansvar op i mange forskellige typer
- At definere grænserne for ansvar forkert
Det er vigtigt at huske reglen: "det er bedre at fejle i den store ende", eller "hvis du er usikker — del ikke op". Hvis for eksempel din klasse har to ansvarsområder, er den stadig forståelig, og du kan opdele den i to med minimal ændring af klientkoden. At samle et glas fra glasskår er som regel sværere på grund af den udspredte kontekst over flere filer og manglen på de nødvendige afhængigheder i klientkoden.
Det er tid til at runde af
Anvendelsesområdet for SRP er ikke begrænset til OOP og SOLID. Det gælder metoder, funktioner, klasser, moduler, mikrotjenester og tjenester. Det gælder både for 'fighax-fighax-og-til-produkt' samt 'rocket-science' udvikling, og gør verden lidt bedre overalt. Hvis man tænker over det, er det næppe andet end en grundlæggende princip i hele ingeniørfaget. Maskinbygning, kontrolsystemer og alle komplekse systemer bygges af komponenter, og 'underfragmentering' berøver konstruktører fleksibilitet, 'overfragmentering' - effektivitet, og forkerte grænser - fornuft og indre fred.
SRP er ikke noget, der er opfundet af naturen, og det er ikke en del af præcise videnskaber. Det stammer fra vores biologiske og psykologiske begrænsninger. Det er blot en måde at kontrollere og udvikle komplekse systemer ved hjælp af det menneskelige primats hjerne. Det fortæller os, hvordan man dekomponerer et system. Den oprindelige formulering krævede en god portion telepati, men jeg håber, denne artikel let kan klare tågen.
Kilde: habr.com
