Princip enotne odgovornosti,
tudi imenovan princip enotne spremenljivosti — je zelo spolzka tema za razumevanje in precej živahno vprašanje na razgovoru za programerja.
Moje prvo resno srečanje s tem principom se je zgodilo na začetku prvega letnika, ko so nas mlade in neizkušene odpeljali v gozd, da bi nas spremenili iz ličink študentov v prave študente.
V gozdu so nas razdelili v skupine po 8-9 ljudi in organizirali tekmovanje — katera skupina bo hitreje spila steklenico vodke, pri čemer prvi iz skupine nalije vodko v kozarce, drugi jo spije, tretji pa ugrizne. Enota, ki je opravila svojo nalogo, se postavi na konec vrste skupine.
Primer, ko je bila dolžina vrste deljiva s tremi, je bila dobra realizacija SRP.
Opredelitev 1. Enotna odgovornost.
Uradna definicija principa enotne odgovornosti (SRP) pravi, da ima vsak objekt svojo odgovornost in razlog za obstoj, ta odgovornost pa je le ena.
Oglejmo si objekt „Pijanček“ (Tippler).
Da bi izvajali princip SRP, razdelimo naloge na tri:
- Eden nalije (PourOperation)
- Eden spije (DrinkUpOperation)
- Eden ugrizne (TakeBiteOperation)
Vsak od udeležencev v procesu je odgovoren za eno komponento procesa, kar pomeni, da ima eno atomarno odgovornost — spiti, naliti ali ugrizniti.
Pijanček pa je v tem primeru fasada za te operacije:
class Tippler {
//...
void Act(){
_pourOperation.Do() // naliti
_drinkUpOperation.Do() // spiti
_takeBiteOperation.Do() // ugrizniti
}
}
Zakaj?
Programer piše kodo za človeka-opico, človek-opica pa je nepazljiv, neumen in nenehno nekam hiti. Ob drži lahko obdrži in razume približno 3 — 7 terminov naenkrat.
V primeru pijančka so ti termini trije. Vendar, če bomo napisali kodo kot eno dolgo vrstico, se bodo pojavili roke, kozarci, pretepi in neskončne razprave o politiki. In vse to bo v telesu enega samega metoda. Prepričan sem, da ste takšno kodo že videli v praksi. Ni ravno najbolj humano preizkušnje za psiho.
Na drugi strani pa je človek-obožavalec osredotočen na mentalno modeliranje objektov iz resničnega sveta. V svoji domišljiji jih lahko tekmuje, sestavlja nove objekte in jih prav tako razbira. Predstavljajte si star model avtomobila. V svoji domišljiji lahko odprete vrata, odprete ploščo vrat in vidite mehanizme dvigal stekel, znotraj katerih bodo zobniki. Ampak ne morete videti vseh komponent avtomobila hkrati, v enem "seznamu". Vsaj človek-obožavalec ne more.
Zato programerski ljudje dekomponirajo zapletene mehanizme na niz manj zapletenih in delujočih elementov. Vendar je mogoče dekomponirati na različne načine: pri mnogih starih avtomobilih zrakoprovod izhaja v vrata, pri sodobnih pa okvara elektronskega zaklepa ne omogoča zagon motorja, kar povzroča težave pri popravilu.
Torej, SRP je princip, ki pojasnjuje KAK dekomponirati, tj. kje potegniti črto ločitve..
Pravi, da je treba dekomponirati po načelu ločitve "odgovornosti", torej glede na naloge določenih objektov.
Vrnimo se k pijančku in prednostim, ki jih dobi človek-obožavalec pri dekomponiranju:
- Koda je postala izjemno jasna na vsakem nivoju.
- Kodo lahko piše več programerjev hkrati (vsak piše poseben element).
- Pojavi se enostavnejše avtomatsko testiranje - čim preprostejši element, tem lažje ga je testirati.
- Pojavi se kompozicija kode - lahko zamenjate DrinkUpOperation z operacijo, v kateri pijanček nalije tekočino pod mizo. Ali zamenjate operacijo nalivanja z operacijo, v kateri mešate vino in vodo ali vodko in pivo. Glede na zahteve podjetja lahko naredite vse to, ne da bi se dotaknili kode metode. Tippler.Act.
- Iz teh operacij lahko sestavite mačka (uporabite le TakeBitOperation), alkoholičarja (uporabite le DrinkUpOperation direktno iz steklenice) in zadovoljite še mnoge druge poslovne zahteve.
(Ojoj, zdi se, da je to že načelo OCP, in sem kršil odgovornost te objave.)
In seveda slabosti:
- Bodo morali ustvariti več tipov.
- Pijanček bo prvič pil za nekaj ur kasneje, kot bi lahko.
Definicija 2. Enotna spremenljivost.
Dovolite, da dami! Razred pijanice prav tako izpolnjuje enotno odgovornost – pije! In sploh, beseda 'odgovornost' je izjemno zamegljena pojem. Nekdo je odgovoren za usodo človeštva, nekdo pa za dvig obrnjenih pingvinov na polu.
Oglejmo si dve izvedbi pijanice. Prva, omenjena zgoraj, vsebuje tri razrede – naliti, spiti in prigrizniti.
Druga je napisana po metodologiji 'Naprej in le naprej' in vsebuje vse logike v metodi 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);
}
}
}Oba ta razreda, z vidika zunanjega opazovalca, izgledata povsem enako in izpolnjujeta enotno odgovornost 'spiti'.
Neprestano!
Takrat se podamo na internet in odkrijemo drugo definicijo SRP – Načelo edinstvene spreminljivosti (Single Changeability Principle).
SCP pravi, da "Modul ima en in samo en razlog za spremembo". To pomeni, 'Odgovornost je razlog za spremembo'.
(Zdi se, da so fantje, ki so prva definicija zamislili, bili prepričani v telepatske sposobnosti človeka-opice)
Zdaj vse postane na svoje mesto. Ločeno lahko spreminjamo postopke nalivanja, pitja in prigrizovanja, samemu pijanici pa lahko spremenimo le zaporedje in sestavo operacij, na primer, premestimo prigrizek pred pitjem ali dodamo branje toastov.
V pristopu 'Naprej in le naprej', vse kar lahko spremenimo – se spremeni le v metodi Act. To je lahko berljivo in učinkovito, kadar je logike malo in se redko spreminja, vendar pogosto konča z groznimi metodami po 500 vrstic v vsaki, s številom if-ov večjim, kot je potrebnega za vstop Rusije v NATO.
Definicija 3. Lokalizacija sprememb.
Pijanice pogosto ne razumejo, zakaj so se zbudili v tuji hiši ali kje je njihov mobilni telefon. Prišel je čas, da dodamo podrobno beleženje.
Začnimo z beleženjem postopka nalivanja:
class PourOperation: IOperation{
PourOperation(ILogger log /*....*/){/*...*/}
//...
void Do(){
_log.Log($"Pred nalivanjem s {_hand} in {_bottle}");
//Poslovna logika nalivanja ...
_log.Log($"Po nalivanju s {_hand} in {_bottle}");
}
}Inkapusliramo jo v PourOperation, smo smo ravnali pametno s stališča odgovornosti in inkapsulacije, vendar pa imamo zdaj z načelom spremenljivosti težave. Poleg same operacije, ki se lahko spreminja, se spreminja tudi sama beležitev. Treba bo ločiti in narediti poseben beležnik za operacijo nalivanja:
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{
//... poslovna logika
_log.LogAfter(_hand, _bottle);
}
catch(exception e){
_log.OnError(_hand, _bottle, e)
}
}
}Natančen bralec bo opazil, da LogAfter, LogBefore in OnError lahko spremenijo tudi ločeno, in po analogiji s prejšnjimi dejanji bomo ustvarili tri razrede: PourLoggerBefore, PourLoggerAfter in PourErrorLogger.
In če se spomnimo, da so operacije za pivca tri — dobimo devet razredov beleženja. Na koncu ima celotna pijača 14 (!!!) razredov.
Hipotetika? Malomarno! Človek-opica z dekompozicijsko granato bo razbil "nalivalca" na vrč, kozarec, operaterje nalivanja, storitev za oskrbo z vodo, fizični model trčenja molekul in naslednji kvartal bo poskušal razvozlati odvisnosti brez globalnih spremenljivk. In verjemite — ne bo se ustavil.
Na tem mestu mnogi pridemo do sklepa, da je SRP zgolj pravljica iz rožnatih kraljestev in gredo tkati testenine…
… ne da bi spoznali obstoj tretje opredelitve Srp:
„Načelo enotne odgovornosti pravi, da podobne stvari, ki se spreminjajo, morajo biti shranjene na enem mestu‘ ali “To, kar se spreminja skupaj, mora biti shranjeno na enem mestu”
To pomeni, da, če spreminjamo beleženje operacije, to moramo spremeniti na enem mestu.
To je zelo pomembna točka — ker so vsa dosedanja pojasnila SRP govorila o tem, da moramo razdeliti tipe, dokler jih je mogoče razdeliti, kar je nalagalo "zgornjo omejitev" na velikost objekta, zdaj govorimo že o "spodnji omejitvi". Z drugimi besedami, SRP ne zahteva le „razdelitve, dokler se razdeli“, ampak tudi, da ne pretiravamo — „ne razdelimo povezanih stvari“. To je velika bitka med Ockamovo britvico in človekom-opico!
Zdaj bi moral pivcu postati lažje. Poleg tega, da ni treba razdeliti beležnika IPourLogger na tri razrede, lahko tudi združimo vse beležnike v en tip:
class OperationLogger{
public OperationLogger(string operationName){
/*..*/}
public void LogBefore(object[] args){
/*...*/}
public void LogAfter(object[] args){
/*..*/}
public void LogError(object[] args, exception e){
/*..*/}
}In caso venga aggiunto un quarto tipo di operazione, la registrazione è già pronta per lui. E il codice stesso delle operazioni è pulito e privo di rumore infrastrutturale.
Di conseguenza, abbiamo 5 classi per risolvere il problema del bere:
- Operazione di versamento
- Operazione di consumo
- Operazione di stuzzichino
- Registratore
- Facciata del bevitore
Ognuno di essi è responsabile in modo rigoroso di un'unica funzionalità e ha una sola ragione per cambiare. Tutte le regole simili per modifiche sono collocate nelle vicinanze.
Esempio dalla vita reale
Un giorno stavamo scrivendo un servizio di registrazione automatica per clienti B2B. E ci fu un metodo GOD di 200 righe con contenuto simile:
- Vai in 1C e crea un conto
- Con questo conto, vai al modulo di pagamento e crealo lì
- Controlla che non sia stato creato un account con tale conto nel principale strežnikom
- Crea un nuovo account
- Aggiungi il risultato della registrazione nel modulo di pagamento e il numero 1C al servizio dei risultati della registrazione
- Aggiungi a questa tabella le informazioni sull'account
- Crea un numero di punto per questo cliente nel servizio punti. Trasmetti a questo servizio il numero del conto 1C.
E in questo elenco c'erano anche circa 10 operazioni aziendali con una terribile interconnessione. L'oggetto conto era necessario per quasi tutti. L'identificatore del punto e il nome del cliente erano necessari in metà delle chiamate.
Dopo un'ora di refactoring, siamo riusciti a separare il codice infrastrutturale e alcune sfumature di lavoro con l'account in metodi/classi separate. Il metodo God si è alleggerito, ma sono rimaste 100 righe di codice che non volevano essere sciolte.
Solo dopo alcuni giorni è arrivata la consapevolezza che l'essenza di questo metodo "alleggerito" è l'algoritmo aziendale. E che la descrizione iniziale del BRIEF era piuttosto complessa. Ed è proprio il tentativo di suddividere questo metodo in pezzi a violare il SRP, e non il contrario.
Formalismo.
È tempo di lasciare in pace il nostro bevitore. Asciuga le lacrime: torneremo sicuramente da lui in futuro. Ma ora formalizziamo le conoscenze di questo articolo.
Formalismo 1. Definizione di SRP
- Separare gli elementi in modo che ciascuno di essi sia responsabile di una sola cosa.
- La responsabilità si traduce in "motivo di cambiamento". Cioè, ogni elemento ha un solo motivo di cambiamento, in termini di logica aziendale.
- Potencialne spremembe poslovne logike je treba lokalizirati. Spremenljivi sinhronizirani elementi morajo biti blizu drug drugega.
Formalizem 2. Potrebni kriteriji samopreverjanja.
Nisem naletel na dovolj kriterijev za izvrševanje SRP. Vendar pa so na voljo potrebni pogoji:
1) Postavite si vprašanje — kaj ta razred/metoda/modul/storitev počne. Na njega morate odgovoriti s preprosto definicijo. (hvala )
pojasnila
Vendar pa je včasih zelo težko najti preprosto definicijo.
2) Popravljanje določenega hrošča ali dodajanje nove funkcionalnosti zahteva minimalno število datotek/razredov. Idealno — ena.
pojasnila
Ker je odgovornost (za funkcionalnost ali hrošča) inkapsulirana v eni datoteki/razredu, natančno veste, kje iskati in kaj popraviti. Na primer: funkcija za spremembo izpisa logiranja operacij bo zahtevala spremembo samo logerja. Ni treba prečesavati celotne kode.
Drug primer — dodajanje novega UI kontrolnika, ki je podoben prejšnjim. Če vas to prisili, da dodate 10 različnih entitet in 15 različnimi konverterji — se zdi, da ste "predelali".
3) Če več razvijalcev dela na različnih funkcionalnostih vašega projekta, je verjetnost za konflikt med združevanjem — torej verjetnost, da bo ista datoteka/razred spremenjena pri več razvijalcih hkrati — minimalna.
pojasnila
Če ob dodajanju nove operacije "Izlij vodko pod mizo" potrebujete vnesti loger, operacijo pitja in nalivanja — potem se zdi, da so odgovornosti krivo razdeljene. Seveda, to ni vedno mogoče, a truditi se je treba zmanjšati to mero.
4) Ob pojasnjevalnem vprašanju glede poslovne logike (od razvijalca ali menedžerja) se ozirate zgolj v en razred/datoteko in dobite informacije le od tam.
pojasnila
Funkcije, pravila ali algoritmi so kompaktno napisani vsak na enem mestu, ne pa raztreseni po vsej kodi.
5) Imenovanje je jasno.
pojasnila
Naš razred ali metoda je odgovorna za eno samo stvar, odgovornost pa je odražena v njenem imenu.
AllManagersManagerService — verjetno Božji razred.
LocalPayment — verjetno ni.
Formalizem 3. Metodika razvoja "Ockham-first".
Na začetku načrtovanja, človek-opica ne ve in ne čuti vseh podrobnosti rešenega problema in lahko naredi napako. Napake so lahko različne:
- Ustvarjanje prevelikih objektov, združevanje različnih odgovornosti
- Razdelitev, razdrobljenje enotne odgovornosti na različne tipe
- Napačno določanje meja odgovornosti
Pomembno je zapomniti pravilo: 'bolje je zgrešiti na večjo stran' ali 'če niste prepričani - ne delite'. Če na primer vaš razred združuje dve odgovornosti - je še vedno jasen in ga je mogoče razdeliti na dva z minimalno spremembo odjemalske kode. Zbiranje koščkov stekla v kozarec je praviloma težje zaradi razpršenosti konteksta v več datotekah in pomanjkanja potrebnih odvisnosti v odjemalski kodi.
Čas je, da zaključimo
Širina uporabe SRP ni omejena na OOP in SOLID. Uporabljati se lahko za metode, funkcije, razrede, module, mikroservise in storitve. Uporaben je tako za 'figaks-figaks-in-v-prod' kot za 'rocketscience' razvoj, povsod izboljšuje svet. Če pomislimo, je to skoraj temeljno načelo celotne inženirije. Strojništvo, kontrolni sistemi in pravzaprav vsi zapleteni sistemi - so zgrajeni iz komponent, 'neprekomerno drobljenje' odvzema konstruktorjem fleksibilnost, 'prekomerno drobljenje' - učinkovitost, napačne meje pa - razum in duševni mir.
SRP ni izum narave in ni del natančne znanosti. Raste iz naših bioloških in psiholoških omejitev. To je le način nadzorovanja in razvijanja zapletenih sistemov s človeškim umom-opico. Pove nam, kako dekomponirati sistem. Prvotna formulacija je zahtevala kar nekaj telepatskih spretnosti, a upam, da je ta članek nekoliko razblinil dimno zaveso.
Izvor: habr.com
