Single responsibility principle, Àven kÀnt som principen för enhetligt ansvar,
den Ă€r ocksĂ„ principen för enhetlig förĂ€ndring â en ganska glidig och nervös frĂ„ga under programmerarintervjuer.
Min första seriösa bekantskap med denna princip skedde i början av första terminen, nÀr vi, unga och gröna, blev tagna till skogen för att göra om studenterna till riktiga studenter.
I skogen delades vi upp i grupper om 8-9 personer i varje och en tĂ€vling anordnades â vilken grupp som snabbast kunde dricka en flaska vodka, med förutsĂ€ttningen att den första personen i gruppen hĂ€ller vodka i glaset, den andra dricker och den tredje Ă€ter nĂ„got. Den som genomfört sin uppgift stĂ€ller sig sist i kön.
Situationen dÀr kön var delbar med tre var en bra implementering av SRP.
Definition 1. Enhetligt ansvar.
Den officiella definitionen av principen för enhetligt ansvar (SRP) sÀger att varje objekt har ett eget ansvar och skÀl att existera, och detta ansvar Àr endast ett.
LÄt oss betrakta objektet "Tippler" (Tippler).
För att tillÀmpa SRP delar vi upp ansvaret pÄ tre:
- En hÀller (PourOperation)
- En dricker (DrinkUpOperation)
- En Àter (TakeBiteOperation)
Varje deltagare i processen ansvarar för en komponent av processen, vilket innebĂ€r att de har ett atomĂ€rt ansvar â att dricka, hĂ€lla eller Ă€ta.
Tipplern Àr Ä sin sida en fasad för dessa operationer:
class Tippler {
//...
void Act(){
_pourOperation.Do() // hÀlla
_drinkUpOperation.Do() // dricka
_takeBiteOperation.Do() // Àta
}
}
Varför?
MÀnniskan-programmeraren skriver kod för mÀnniskan-apan, och mÀnniskan-apan Àr ouppmÀrksam, dum och rusar alltid vidare nÄgonstans. Han kan hÄlla och förstÄ cirka 3 till 7 termer vid ett och samma tillfÀlle.
I fallet med Tipplern Àr dessa termer tre. Men om vi skriver koden som en lÄng text, sÄ dyker hÀnder, glas, slagsmÄl och oÀndliga diskussioner om politik upp. Och allt detta kommer att vara i kroppen av en metod. Jag Àr sÀker pÄ att du har sett sÄdan kod i din praktik. Det Àr inte det mest humana provet för psyket.
à andra sidan Àr mÀnniskan-apa inriktad pÄ att modellera verkliga objekt i sitt sinne. I sin fantasi kan han krocka dem, bygga nya objekt och dismantla dem pÄ samma sÀtt. TÀnk dig en gammal bilmodell. Du kan i fantasin öppna dörren, skruva bort dörrens klÀdsel och se mekanismerna för fönsterhissarna, dÀr det finns kugghjul. Men du kan inte se alla bilens komponenter samtidigt, i en "lista". à tminstone kan "mÀnniskan-apa" inte.
DÀrför dekomponerar programmerande mÀnniskor komplexa mekanismer i en uppsÀttning mindre komplexa och fungerande element. Men de kan dekomponera pÄ olika sÀtt: i mÄnga gamla bilar gÄr luftkanalen ut i dörren, medans en modern bils elektroniska lÄs kan misslyckas och hindra motorn frÄn att starta, vilket kan vara frustrerande vid reparation.
SÄ SRP Àr en princip som förklarar HUR man dekomponerar, det vill sÀga var man ska dra linjen för uppdelning..
Det sÀger att man ska dekomponera utifrÄn principen om "ansvarsfördelning", det vill sÀga utifrÄn uppgifterna hos olika objekt.
LÄt oss ÄtervÀnda till dryckaren och de fördelar som mÀnniskan-apa fÄr vid dekomponering:
- Koden blir mycket tydlig pÄ varje nivÄ.
- Koden kan skrivas av flera programmerare samtidigt (var och en skriver ett separat element).
- Automatiserad testning underlĂ€ttas â ju enklare elementet Ă€r, desto lĂ€ttare Ă€r det att testa.
- Kodens komposition framtrĂ€der â det gĂ„r att byta ut DrinkUpOperation mot en operation dĂ€r dryckaren hĂ€ller ut vĂ€tska under bordet. Eller byta ut hĂ€lloperationen mot en dĂ€r du blandar vin och vatten eller vodka och öl. Beroende pĂ„ affĂ€rens krav kan du göra allt detta utan att röra koden för metoden. Tippler.Act.
- UtifrÄn dessa operationer kan du kombinera glutton (anvÀnder bara TakeBitOperation), alkoholisten (anvÀnder bara DrinkUpOperation direkt frÄn flaskan) och uppfylla mÄnga andra affÀrsbehov.
(OjdÄ, ser ut som detta redan Àr en OCP-princip, och jag har brutit mot detta inlÀggets ansvar.)
Och sÄklart, nackdelarna:
- Det kommer att krÀvas att skapa fler typer.
- Dryckaren kommer att dricka ett par timmar senare Àn han skulle kunna.
Definition 2. Enhetlig förÀndring.
LĂ„t oss vara Ă€rliga, herrar! Klassen av drinkare har ocksĂ„ ett enda ansvar â att dricka! Och överhuvudtaget Ă€r ordet "ansvar" ett extremt diffust begrepp. NĂ„gon Ă€r ansvarig för mĂ€nsklighetens öde, medan nĂ„gon annan Ă€r ansvarig för att hjĂ€lpa upp slipade pingviner pĂ„ polen.
LĂ„t oss betrakta tvĂ„ implementationer av drinkaren. Den första, som nĂ€mndes ovan, innehĂ„ller tre klasser â hĂ€lla, dricka och snaska.
Den andra, skriven med metodologin "FramÄt och bara framÄt", innehÄller all 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);
}
}
}BÄda dessa klasser, frÄn en extern observatörs perspektiv, ser helt lika ut och har det enda ansvaret "att dricka".
Klausul!
DĂ„ tar vi oss en titt pĂ„ internet och lĂ€r oss en annan definition av SRP â Principen om enstaka förĂ€ndring (Single Changeability Principle).
SCP sÀger att "Modulen har en och enbart en anledning till förÀndring". Det vill sÀga "Ansvar Àr anledningen till förÀndring".
(Tydligen var killarna som kom pÄ den ursprungliga definitionen övertygade om mÀnniskans telepatiska förmÄgor)
Nu faller allt pÄ plats. Separat kan vi Àndra procedurerna för att hÀlla, dricka och snaska, och i sjÀlva drinkaren kan vi bara Àndra sekvensen och sammansÀttningen av operationerna, till exempel genom att flytta snacket till före drickandet eller lÀgga till en skÄl.
I ansatsen "FramĂ„t och bara framĂ„t" kan det som gĂ„r att Ă€ndra â Ă€ndras endast i metoden Act. Detta kan vara lĂ€sbart och effektivt nĂ€r logiken Ă€r minimal och sĂ€llan förĂ€ndras, men ofta slutar det med hemska metoder pĂ„ 500 rader vardera, med fler if-satser Ă€n som krĂ€vs för att Ryssland ska gĂ„ med i NATO.
Definition 3. Lokalisering av förÀndringar.
Drinkare förstÄr ofta inte varför de vaknar i nÄgon annans lÀgenhet, eller var deras mobiltelefon Àr. Det Àr dags att lÀgga till detaljerad loggning.
Vi börjar loggningen med hÀllprocessen:
class PourOperation: IOperation{
PourOperation(ILogger log /*....*/){/*...*/}
//...
void Do(){
_log.Log($"Innan hÀllning med {_hand} och {_bottle}");
//HÀll affÀrslogik ...
_log.Log($"Efter hÀllning med {_hand} och {_bottle}");
}
}Inkaplulerar den i PourOperation, vi har agerat klokt nÀr det gÀller ansvar och inkapsling, men nu har vi en klurig situation nÀr det gÀller förÀnderliga principer. Utöver sjÀlva operationen, som kan förÀndras, blir Àven loggningen förÀndlig. Vi kommer att behöva separera och skapa en specialiserad loggare för hÀlloperationen:
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{
//... affÀrslogik
_log.LogAfter(_hand, _bottle);
}
catch(exception e){
_log.OnError(_hand, _bottle, e);
}
}
}Den noggranna lÀsaren kommer att mÀrka att LogAfter, LogBefore och OnError kan ocksÄ förÀndras separat, och i likhet med tidigare ÄtgÀrder kommer tre klasser att skapas: PourLoggerBefore, PourLoggerAfter och PourErrorLogger.
Och med tanke pÄ att det finns tre operationer för hÀllandet, fÄr vi nio loggningsklasser. Totalt bestÄr hÀlloperationen av 14 (!!!) klasser.
Hyperbol? Sannolikt inte! MÀnniskoskapelsen med en dekompositionsgranat kommer att krossa "hÀllaren" i karaff, glas, hÀlloperatörer, vattenleveransservice, den fysiska modellen för molekylkollisioner, och under nÀsta kvartal kommer den att försöka reda ut beroenden utan globala variabler. Och tro mig - den kommer inte att stanna.
Just vid detta tillfÀlle kommer mÄnga till slutsatsen att SRP Àr sagor frÄn rosafÀrgade kungadömen och gÄr ivÀg for att skapa illusioner...
... utan att veta om existensen av den tredje definitionen av SRP:
«Principen om enhetligt ansvar sĂ€ger att liknande objekt för förĂ€ndring bör förvaras pĂ„ ett stĂ€lle». eller âDet som förĂ€ndras tillsammans bör förvaras pĂ„ ett stĂ€lle.â
Det vill sÀga, om vi Àndrar loggningen av operationen, sÄ mÄste vi göra det pÄ ett stÀlle.
Detta Àr en mycket viktig punkt - eftersom alla tidigare förklaringar av SRP handlade om att man ska dela typer, sÄ lÀnge de kan delas, dÀrmed lade det "övre begrÀnsningar" pÄ objektets storlek, och nu talar vi Àven om "nedre begrÀnsningar". Med andra ord, SRP krÀver inte bara att man "delar sÄ lÀnge det gÄr", utan ocksÄ att man inte överdriver - "inte dela sammanbundna saker".. Detta Àr det stora slaget mellan Ockhams rakkniv och mÀnniskoskapelsen!
Nu bör det bli lÀttare för hÀllaren. Förutom att vi inte behöver dela loggaren IPourLogger i tre klasser, kan vi ocksÄ samla alla loggare i en typ:
class OperationLogger{
public OperationLogger(string operationName){
/*..*/}
public void LogBefore(object[] args){
/*...*/}
public void LogAfter(object[] args){
/*..*/}
public void LogError(object[] args, exception e){
/*..*/}
}Och om vi lÀgger till en fjÀrde typ av operation, sÄ Àr loggningen redan klar för den. Och koden för sjÀlva operationerna Àr ren och fri frÄn infrastruktursbrus.
Resultatet Àr att vi har 5 klasser för att lösa problemet med drickande:
- HĂ€gningsoperation
- Uppdragsutförande
- Ătandeoperation
- Loggskribent
- Bara för drickande
Varje klass ansvarar strikt för en funktionalitet och har ett enda skÀl för förÀndring. Alla liknande regler för förÀndring ligger nÀra varandra.
Exempel frÄn verkliga livet
En gÄng skrev vi en tjÀnst för automatisk registrering av b2b-kunder. Och en GOD-metod pÄ 200 rader liknande innehÄll dök upp:
- GĂ„ till 1C och registrera ett konto
- Med det kontot, gÄ till betalningsmodulen och registrera det dÀr
- Kontrollera att ett konto med det kontot inte redan finns i huvudsystemet servern
- Skapa ett nytt konto
- Registreringsresultatet i betalningsmodulen och numret 1C lÀggs till i registreringstjÀnsten
- LÀgg till information om kontot i den hÀr tabellen
- Skapa en punktnummer för den hÀr kunden i punkttjÀnsten. Skicka punktnumret 1C till den tjÀnsten.
Och det fanns omkring 10 andra affÀrsoperationer i den hÀr listan med fruktansvÀrd koppling. Kontoobjektet behövdes av nÀstan alla. Punktidentifierare och kundnamn behövdes i hÀlften av anropen.
Efter en timmes refaktorering kunde vi separera infrastrukturskoden och vissa nyanser av arbete med kontot i separata metoder/klasser. Godmetoden blev lÀttare, men fortfarande fanns det 100 rader kod som inte ville lösa sig.
Först efter nÄgra dagar kom förstÄelsen att kÀrnan i denna "lÀttare" metod Àr sjÀlva affÀrsalgoritmen. Och att den ursprungliga specifikationen av krav var ganska komplex. Och just försöket att dela upp denna metod skulle vara ett brott mot SRP, snarare Àn tvÀrtom.
Formalism.
Det Àr dags att lÀmna vÄr drickare i fred. Torka tÄrarna - vi kommer definitivt att Äterkomma till honom nÄgon gÄng. Men nu formaliserar vi kunskaperna frÄn denna artikel.
Formalism 1. Definition av SRP
- Separera element sÄ att var och en av dem ansvarar för nÄgot enskilt.
- Ansvar översÀtts som "skÀl för förÀndring". Det vill sÀga varje element har bara ett skÀl för förÀndring, i termer av affÀrslogik.
- Potentiella förĂ€ndringar av affĂ€rslogik mĂ„ste lokaliseras. Ăndringsbara synkrona element bör placeras nĂ€ra varandra.
Formalism 2. NödvÀndiga sjÀlvgenererade kriterier.
Jag har inte stött pÄ tillrÀckliga kriterier för att uppfylla SRP. Men det finns nödvÀndiga villkor:
1) StÀll dig sjÀlv frÄgan - vad gör denna klass/metod/modul/tjÀnst. Du bör kunna svara pÄ den med en enkel definition. (Tack) )
förklaringar
Det Àr dock ibland mycket svÄrt att hitta en enkel definition
2) Fixandet av ett visst fel eller tillÀgg av en ny funktion pÄverkar ett minimalt antal filer/klasser. Idealiskt - en.
förklaringar
Eftersom ansvaret (för funktionen eller felet) Àr inkapslat i en fil/klass, vet du exakt var du ska leta och vad du ska Àndra. Till exempel: funktionen för att Àndra loggningen av operationer krÀver endast att loggaren Àndras. Det behövs inte att jaga runt i resten av koden.
Ett annat exempel - tillÀgget av en ny UI-kontroll, liknande tidigare. Om detta fÄr dig att behöva lÀgga till 10 olika enheter och 15 olika konverterare - tycks det som om du har "överarbetat".
3) Om flera utvecklare arbetar med olika funktioner i ditt projekt, sÄ Àr risken för merge-konflikt, det vill sÀga risken för att samma fil/klass Àndras av flera utvecklare samtidigt - minimal.
förklaringar
Om tillÀgget av en ny operation "HÀlla vodka under bordet" krÀver att du berör loggaren, operationshÀllning och -drickande - tyder det pÄ att ansvarigheten har delats upp dÄligt. SjÀlvklart Àr detta inte alltid möjligt, men det Àr viktigt att försöka minska detta mÄtt.
4) Vid klargörande frÄgor om affÀrslogik (frÄn utvecklaren eller managern) grÀver du strikt i en enda klass/fil och fÄr informationen endast dÀrifrÄn.
förklaringar
Funktioner, regler eller algoritmer skrivs kompakt, var och en pÄ en plats, och inte spridda som flaggor över hela kodutrymmet.
5) Namngivningen Àr tydlig.
förklaringar
VÄr klass eller metod Àr ansvarig för nÄgot enda, och ansvaret Äterspeglas i dess namn
AllManagersManagerService - Àr förmodligen en Gud-klass
LocalPayment - sannolikt inte
Formalism 3. Utvecklingsmetodik "Ockham-first".
I början av designen vet inte apmannen alla nyanser av det problem som ska lösas och kan göra misstag. Man kan fela pÄ olika sÀtt:
- Att göra för stora objekt genom att klistra samman olika ansvar
- Att dela upp ett enhetligt ansvar i mÄnga olika typer
- Felaktigt definiera grÀnser för ansvar
Det Àr viktigt att komma ihÄg regeln: "bÀttre att göra misstag Ät det stora hÄllet", eller "om du Àr osÀker - dela inte upp". Om till exempel din klass har tvÄ ansvar - sÄ Àr den fortfarande förstÄelig och kan delas upp i tvÄ med minimal förÀndring av kundkoden. Att samla ett glas av krossat glas Àr i regel svÄrare pÄ grund av det spridda sammanhanget över flera filer och bristen pÄ nödvÀndiga beroenden i kundkoden.
Det Àr dags att avrunda
TillÀmpningsomrÄdet för SRP Àr inte begrÀnsat till OOP och SOLID. Det Àr tillÀmpligt pÄ metoder, funktioner, klasser, moduler, mikrotjÀnster och tjÀnster. Det Àr tillÀmpligt bÄde pÄ "fixa-fixa-och-i-produktion" och "raketvetenskap" utveckling, och gör vÀrlden lite bÀttre överallt. Om man tÀnker efter Àr det knappt inte en grundlÀggande princip för all ingenjörskonst. Maskinteknik, styrsystem och faktiskt alla komplexa system byggs av komponenter, och "underuppdelning" tar bort konstruktörernas flexibilitet, "överuppdelning" tar bort effektiviteten, och felaktiga grÀnser tar bort förnuft och sjÀlslig ro.
SRP Àr inte upptÀckt av naturen och Àr inte en del av en exakt vetenskap. Det kommer frÄn vÄra biologiska och psykologiska begrÀnsningar. Det Àr helt enkelt ett sÀtt att kontrollera och utveckla komplexa system med hjÀlp av den mÀnskliga apan. Det lÀr oss hur man dekomponerar ett system. Den ursprungliga formuleringen krÀvde en hel del telepatiska fÀrdigheter, men jag hoppas att denna artikel har lyft lite av dimman.
KĂ€lla: habr.com
