Principio di responsabilità singola, noto anche come principio di unica responsabilità,
è un principio estremamente scivoloso per essere compreso e una questione così nervosa durante i colloqui per programmatori.
Il mio primo serio incontro con questo principio è avvenuto all'inizio del primo anno, quando noi giovani e inesperti siamo stati portati nel bosco per trasformare i bruchi in studenti veri.
Nel bosco siamo stati divisi in gruppi di 8-9 persone ciascuno e abbiamo organizzato una competizione: quale gruppo riesce a bere più velocemente una bottiglia di vodka, a patto che il primo della squadra versa la vodka nel bicchiere, il secondo beve e il terzo mangia Snack. L'unità che ha completato la propria operazione si posiziona alla fine della fila del gruppo.
Un caso in cui la dimensione della fila era un multiplo di tre è stato una buona realizzazione del SRP.
Definizione 1. Unica responsabilità.
La definizione ufficiale del principio di unica responsabilità (SRP) afferma che ogni oggetto ha la propria responsabilità e una ragione per esistere, e questa responsabilità è unica.
Consideriamo l'oggetto "Bevitore" (Tippler).
Per applicare il principio SRP dividiamo i compiti in tre:
- Uno versa (PourOperation)
- Uno beve (DrinkUpOperation)
- Uno mangia (TakeBiteOperation)
Ogni partecipante al processo è responsabile di un componente del processo, ovvero ha una responsabilità atomica — bere, versare o mangiare.
Il Bevitore, a sua volta, è un facciata per queste operazioni:
class Tippler {
//...
void Act(){
_pourOperation.Do() // versare
_drinkUpOperation.Do() // bere
_takeBiteOperation.Do() // mangiare
}
}
Perché?
Il programmatore scrive codice per una scimmia, e la scimmia è distratta, stupida e sempre di fretta. Può mantenere e capire circa 3-7 termini in un momento.
Nel caso del Bevitore, queste terminologie sono tre. Tuttavia, se scriviamo il codice in un unico grande blocco, appariranno mani, bicchieri, scazzottate e infinite dispute sulla politica. E tutto ciò sarà all'interno di un unico metodo. Sono sicuro che hai visto un codice del genere nella tua pratica. Non è la prova più umana per la psiche.
D'altra parte, l'uomo-scimmia è bloccato a modellare oggetti del mondo reale nella sua mente. Nella sua immaginazione può scontrarli, assemblarli in nuovi oggetti e smontarli allo stesso modo. Immagina un vecchio modello di auto. Puoi immaginare di aprire la porta, svitare la copertura della porta e vedere i meccanismi dei finestrini elettrici, all'interno dei quali ci sono ingranaggi. Ma non puoi vedere tutti i componenti dell'auto contemporaneamente, in un'unica "listing". Almeno, l'uomo-scimmia non può farlo.
Pertanto, gli uomini-programmatori decomponendo i meccanismi complessi in un insieme di elementi meno complessi e funzionanti. Tuttavia, si può decomporre in modi differenti: in molte auto vecchie, il condotto dell'aria esce dalla porta, e nelle moderne, un guasto all'elettronica della serratura impedisce al motore di avviarsi, causando problemi durante la riparazione.
Quindi, SRP è il principio che spiega COME decomporre, cioè dove tracciare la linea di separazione..
Dice che si deve decomporre secondo il principio di separazione delle "responsabilità", cioè in base ai compiti di determinati oggetti.
Torniamo all'alcolista e ai vantaggi che l'uomo-scimmia ottiene decomponendo:
- Il codice diventa estremamente chiaro a ogni livello.
- Il codice può essere scritto da più programmatori contemporaneamente (ognuno scrive un elemento separato).
- Si semplifica il test automatico: più semplice è l'elemento, più facile è testarlo.
- Si sviluppa la compositività del codice: si può sostituire DrinkUpOperation con un'operazione in cui l'alcolista versa un liquido sotto il tavolo. Oppure sostituire l'operazione di versamento con una operazione in cui mescoli vino e acqua o vodka e birra. A seconda delle esigenze del business, puoi fare tutto ciò, senza toccare il codice del metodo. Tippler.Act.
- Da queste operazioni puoi assemblare un ingordo (utilizzando solo TakeBitOperation), un alcolista (utilizzando solo DrinkUpOperation direttamente dalla bottiglia) e soddisfare molte altre esigenze di business.
(Oh, sembra che questo sia già il principio OCP, e ho violato la responsabilità di questo post).
E, naturalmente, ci sono svantaggi:
- Sarà necessario creare più tipi.
- L'alcolista berrà per la prima volta un paio d'ore più tardi di quanto potrebbe.
Definizione 2. Variazione uniforme.
Permettete, signori! La classe del beone ha una sola responsabilità — bere! E in generale, la parola "responsabilità" è un concetto estremamente vago. Qualcuno è responsabile del destino dell'umanità, mentre qualcun altro è responsabile di sollevare i pinguini rovesciati al polo.
Consideriamo due implementazioni del beone. La prima, sopra menzionata, contiene tre classi: versare, bere e stuzzicare.
La seconda è scritta secondo la metodologia "Avanti e solo avanti" e contiene tutta la logica nel metodo 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);
}
}
}Entrambe queste classi, dal punto di vista di un osservatore esterno, sembrano assolutamente identiche e svolgono la stessa responsabilità di "bere".
Confusione!
Allora andiamo su Internet e scopriamo un'altra definizione dell'SRP — il Principio di Unica Variazione (Single Changeability Principle).
L'SCP afferma che "Un modulo ha un solo e unico motivo per cambiare". In altre parole, "Responsabilità è un motivo per il cambiamento".
(Sembra che i ragazzi che hanno inventato la definizione originale fossero certi delle capacità telepatiche dell'uomo-scimmia)
Ora tutto ha senso. Possiamo cambiare separatamente le procedure di versamento, di consumo e di stuzzicheria, mentre nel vero e proprio beone possiamo modificare solo la sequenza e la composizione delle operazioni, per esempio, spostando la stuzzicheria prima del bere o aggiungendo la lettura di un brindisi.
Nell'approccio "Avanti e solo avanti", tutto ciò che può essere cambiato cambia solo nel metodo Act. Questo può essere leggibile ed efficiente quando la logica è limitata e cambia raramente, ma spesso si traduce in terribili metodi di 500 righe ciascuno, con un numero di if maggiore di quanto richiesto per l'ingresso della Russia nella NATO.
Definizione 3. Localizzazione dei cambiamenti.
I beoni spesso non capiscono perché si siano svegliati in un appartamento straniero, o dove sia il loro cellulare. È tempo di aggiungere un dettagliato logging.
Iniziamo il logging dal processo di versamento:
class PourOperation: IOperation{
PourOperation(ILogger log /*....*/){/*...*/}
//...
void Do(){
_log.Log($"Prima del versamento con {_hand} e {_bottle}");
//Logica di versamento ...
_log.Log($"Dopo il versamento con {_hand} e {_bottle}");
}
}Incorporandola in PourOperation, abbiamo agito saggiamente dal punto di vista della responsabilità e dell'incapsulamento, ma ora abbiamo confusione riguardo al principio di mutabilità. Oltre all'operazione stessa, che può variare, anche il logging diventa mutevole. Dovremo separarlo e creare un logger speciale per l'operazione di versamento:
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{
//... logica aziendale
_log.LogAfter(_hand, _bottle);
}
catch(exception e){
_log.OnError(_hand, _bottle, e);
}
}
}Il lettore attento noterà che LogAfter, LogBefore e OnError possono anch'essi variare separatamente e, analogamente alle azioni precedenti, creerà tre classi: PourLoggerBefore, PourLoggerAfter e PourErrorLogger.
E ricordando che le operazioni per il versatore sono tre, otteniamo nove classi di logging. In totale, il versatore consiste in 14 (!!!) classi.
È un'iperbole? Difficilmente! L'uomo-scimmia con una granata di decomposto frantumerà il 'versatore' in decanter, bicchieri, operatori di versamento, servizio di erogazione dell'acqua, modello fisico di collisione delle molecole e il prossimo trimestre tenterà di districare le dipendenze senza variabili globali. E credetemi — non si fermerà.
Proprio in questo momento molti concludono che il SRP è una favola dei regni rosa e si mettono a 'infilare spaghetti'...
... senza mai scoprire l'esistenza della terza definizione di SRP:
«Il principio di responsabilità unica afferma che le cose simili per la modifica devono essere conservate in un unico posto«. oppure “Ciò che cambia insieme deve essere conservato in un unico posto”
In altre parole, se modifichiamo il logging dell'operazione, dobbiamo farlo in un unico posto.
Questo è un punto molto importante — poiché tutte le spiegazioni del SRP precedenti parlavano di dover suddividere i tipi finché sono suddivisibili, imponendo quindi un «limite superiore» sulla dimensione dell'oggetto, ora parliamo anche di un «limite inferiore». In altre parole, il SRP richiede non solo di «suddividere finché si può», ma anche di non esagerare: «non frammentare le cose collegate». Questa è una grande battaglia tra il rasoio di Occam e l'uomo-scimmia!
Ora il versatore dovrebbe sentirsi più a suo agio. Oltre al fatto che non è necessario suddividere il logger IPourLogger in tre classi, possiamo anche unire tutti i logger in un unico tipo:
class OperationLogger{
public OperationLogger(string operationName){
/*..*/}
public void LogBefore(object[] args){
/*...*/}
public void LogAfter(object[] args){
/*..*/}
public void LogError(object[] args, exception e){
/*..*/}
}E se aggiungeremo un quarto tipo di operazione, la registrazione sarà già pronta. E il codice delle operazioni stesse è pulito e privo di rumore infrastrutturale.
Di conseguenza, abbiamo 5 classi per risolvere il problema dell'imbottigliamento:
- Operazione di riempimento
- Operazione di svuotamento
- Operazione di abbinamento
- Logger
- Facciata dell'imbottigliatore
Ognuna di esse è responsabile di una sola funzionalità, ha una sola causa di cambiamento. Tutte le regole simili per il cambiamento si trovano vicine.
Esempio dalla vita reale
Una volta abbiamo scritto un servizio di registrazione automatica per clienti b2b. E si è presentato il metodo GOD di 200 righe con contenuti simili:
- Vai in 1C e crea un conto
- Con questo conto, vai al modulo di pagamento e crealo lì
- Controlla che l'account con questo conto non sia stato creato nel principale server
- 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 dei punti. Passa a questo servizio il numero di conto 1C.
E c'erano in questo elenco circa 10 operazioni commerciali con una terribile interconnessione. L'oggetto conto era necessario per quasi tutti. L'identificatore del punto e il nome del cliente erano necessari nella metà delle chiamate.
Dopo un'ora di rifattorizzazione, siamo riusciti a separare il codice infrastrutturale e alcune sfumature del lavoro con l'account in metodi/classi separati. Il metodo God si è alleggerito, ma sono rimaste 100 righe di codice che non volevano sciogliersi.
Solo dopo alcuni giorni ho capito che l'essenza di questo metodo 'alleggerito' era il business algorithm. E che la descrizione originale del documento delle specifiche era piuttosto complessa. E proprio il tentativo di spezzare questo metodo sarebbe stata una violazione del SRP, e non il contrario.
Formalismo.
È tempo di lasciare in pace il nostro imbottigliatore. Asciuga le lacrime: torneremo sicuramente a lui in un altro momento. Ma ora formalizziamo le conoscenze da questo articolo.
Formalismo 1. Definizione del SRP
- Separare gli elementi in modo che ognuno di essi sia responsabile di una sola cosa.
- La responsabilità è interpretata come 'motivo di cambiamento'. Vale a dire, ogni elemento ha un solo motivo per cambiare, in termini di logica di business.
- Le potenziali modifiche alla logica aziendale devono essere localizzate. Gli elementi modificabili in modo sincronizzato devono essere vicini.
Formalismo 2. Criteri necessari per l'autovalutazione.
Non ho trovato criteri sufficienti per soddisfare l'SRP. Ma ci sono condizioni necessarie:
1) Fai a te stesso la domanda: cosa fa questa classe/questo metodo/questo modulo/questo servizio. Devi rispondere con una definizione semplice. (grazie )
chiarimenti
Tuttavia, a volte è molto difficile trovare una definizione semplice
2) La correzione di un bug o l'aggiunta di una nuova funzionalità deve interessare il minor numero possibile di file/classi. Idealmente, uno.
chiarimenti
Poiché la responsabilità (per la funzionalità o il bug) è incapsulata in un solo file/classe, sai esattamente dove cercare e cosa modificare. Ad esempio, una funzionalità che cambia l'output della registrazione delle operazioni richiederà di modificare solo il registratore. Non è necessario correre per tutto il resto del codice.
Un altro esempio: aggiungere un nuovo controllo UI simile ai precedenti. Se questo ti costringe ad aggiungere 10 diverse entità e 15 diversi convertitori, sembra che tu abbia "esagerato".
3) Se più sviluppatori stanno lavorando su diverse funzionalità del tuo progetto, la probabilità di conflitto di fusione, cioè la probabilità che lo stesso file/classe venga modificato da diversi sviluppatori contemporaneamente, è minima.
chiarimenti
Se, quando aggiungi una nuova operazione "Versare vodka sotto il tavolo", devi toccare il registratore, l'operazione di bere e versare, sembra che le responsabilità siano state divise male. Certamente, non è sempre possibile, ma bisogna cercare di ridurre questa probabilità.
4) Quando fai una domanda chiarificatrice sulla logica aziendale (da uno sviluppatore o un manager), ti concentri rigorosamente su una sola classe/file e ottieni informazioni solo da lì.
chiarimenti
Le funzionalità, le regole o gli algoritmi sono scritti in modo compatto, ciascuno in un unico posto e non sparsi con flag in tutto il codice.
5) La denominazione è chiara.
chiarimenti
La nostra classe o metodo è responsabile di una sola cosa e la responsabilità è riflessa nel suo nome.
AllManagersManagerService — molto probabilmente un God class.
LocalPayment — probabilmente no.
Formalismo 3. La metodologia di sviluppo "Occam-first".
All'inizio della progettazione, l'uomo-scimpanzé non conosce e non percepisce tutte le sfumature del problema da risolvere e potrebbe commettere errori. Gli errori possono avvenire in vari modi:
- Creare oggetti troppo grandi, accorpando diverse responsabilità
- Ridistribuire, suddividendo una responsabilità unica in molti tipi diversi
- Definire erroneamente i confini delle responsabilità
È importante ricordare la regola: «è meglio sbagliarsi per eccesso», o «se non si è sicuri, non suddividere». Se, ad esempio, la tua classe raccoglie due responsabilità — rimane comunque comprensibile e può essere divisa in due con un minimo cambiamento del codice client. Creare un bicchiere dai frammenti di vetro, di solito, è più difficile a causa del contesto disperso su più file e della mancanza delle dipendenze necessarie nel codice client.
È tempo di concludere
L'ambito di applicazione del SRP non si limita alla OOP e al SOLID. È applicabile a metodi, funzioni, classi, moduli, microservizi e servizi. È valido sia per lo sviluppo “figacs-figacs-e-in-prod” che per il “rocket-science”, migliorando ovunque il mondo un po' alla volta. A pensarci bene, è quasi un principio fondamentale di tutta l'ingegneria. L'ingegneria meccanica, i sistemi di controllo e, in generale, tutti i sistemi complessi — sono costruiti da componenti, e il “non suddividere” priva i progettisti di flessibilità, il “suddividere eccessivamente” di efficienza, mentre confini errati privano della razionalità e della tranquillità.
Il SRP non è stato inventato dalla natura e non fa parte delle scienze esatte. Emerse dalle nostre limitazioni biologiche e psicologiche. È solo un modo per controllare e sviluppare sistemi complessi utilizzando il cervello di un primate umano. Ci indica come decomporre un sistema. La formulazione iniziale richiedeva notevoli abilità di telepatia, ma spero che questo articolo abbia un po' dissipato la nebbia.
Fonte: habr.com
