Principio di Responsabilità Unica. Non così semplice come sembra

Principio di Responsabilità Unica. Non così semplice come sembra Principio di responsabilità singola, ovvero il principio di unica responsabilità,
che è anche il principio di unica mutabilità — un argomento difficile da comprendere e un tema piuttosto delicato da affrontare durante un colloquio per programmatori.

Il mio primo serio incontro con questo principio è avvenuto all'inizio del primo anno, quando noi, giovani e inesperti, siamo stati portati in una foresta per trasformarci da larve di studenti in veri studenti.

Nella foresta, 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 la prima persona del gruppo versa la vodka nel bicchiere, la seconda beve e la terza mangia uno stuzzichino. L'unità che ha completato la sua operazione si mette in fondo alla fila del gruppo.

Un caso in cui la dimensione della fila era multipla di tre è stata una buona implementazione del SRP.

Definizione 1. Unica responsabilità.

La definizione ufficiale del principio di unica responsabilità (SRP) afferma che ogni oggetto ha la sua responsabilità e motivo di esistenza, e questa responsabilità è unica.

Consideriamo l'oggetto ‘Vincitore di bevute’ (Tippler).
Per applicare il principio SRP, dividiamo i compiti in tre parti:

  • Uno versa (OperazioneVersamento)
  • Uno beve (OperazioneBevuta)
  • Uno morda (OperazioneMordere)

Ogni partecipante al processo è responsabile di un componente del processo, ossia ha una responsabilità atomica — bere, versare o masticare.

Il bevitore, a sua volta, è un facciata per queste operazioni:

class Bevitore {
    //...
    void Azione() {
        _operazioneVersamento.Fai() // versare
        _operazioneBevuta.Fai() // bere
        _operazioneMordere.Fai() // masticare
    }
}

Principio di Responsabilità Unica. Non così semplice come sembra

Perché?

L'uomo-programmatore scrive codice per l'uomo-scimmia, e l'uomo-scimmia è distratto, stupido e sempre di fretta. Può tenere e comprendere circa 3-7 termini alla volta.
Nel caso del bevitore, questi termini sono tre. Tuttavia, se scriviamo codice su un unico foglio, appariranno mani, bicchieri, risse e infinite discussioni sulla politica. E tutto questo sarà nel corpo di un unico metodo. Sono sicuro che hai visto questo codice nella tua pratica. Non è la prova più umana per la psiche.

D'altra parte, l'essere umano è dotato di un'abilità unica nel modellare nella propria mente gli oggetti del mondo reale. Nella sua immaginazione, può combinarli, creare nuovi oggetti e allo stesso modo, distruggerli. Immaginate un vecchio modello di auto. Potete mentalmente aprire la porta, svitare la copertura della porta e vedere all'interno i meccanismi dei finestrini elettrici, con le loro ingranaggi. Ma non potete vedere tutti i componenti dell'auto contemporaneamente, in un'unica 'visione complessiva'. Almeno, l' 'essere umano' non può farlo.

Pertanto, i programmatori umani decompongono meccanismi complessi in un insieme di elementi meno complessi e funzionanti. Tuttavia, la decomposizione può avvenire in modi diversi: in molte auto più vecchie, il condotto dell'aria passa nella porta, mentre in quelle moderne, un guasto elettronico della serratura impedisce all'auto di avviarsi, creando problemi durante la riparazione.

Quindi, SRP è un principio che spiega COME decomporsi, ovvero dove tracciare la linea di separazione..

Affermando che la decomposizione deve avvenire secondo il principio di separazione 'delle responsabilità', cioè in base ai compiti di vari oggetti.

Principio di Responsabilità Unica. Non così semplice come sembra

Torniamo al bere e ai vantaggi che ottiene l'uomo-scimmia nella decomposizione:

  • Il codice è diventato estremamente chiaro a ogni livello
  • Il codice può essere scritto da più programmatori contemporaneamente (ognuno scrive un elemento separato)
  • È semplificato il test automatico: più semplice è l'elemento, più facile è testarlo
  • Si ottiene la composizione del codice: è possibile sostituire OperazioneBevuta con un'operazione in cui l'uomo-scimmia versa un liquido sotto il tavolo. Oppure sostituire l'operazione di versamento con un'operazione in cui mescoli vino e acqua o vodka e birra. A seconda delle esigenze aziendali, puoi fare di tutto, senza toccare il codice del metodo Tippler.Act.
  • Da queste operazioni puoi comporre un mangione (utilizzando solo TakeBitOperation), un alcolizzato (utilizzando solo OperazioneBevuta direttamente dalla bottiglia) e soddisfare molte altre esigenze aziendali.

(Oh, sembra che questo sia già il principio OCP, e ho violato la responsabilità di questo post)

E, naturalmente, i svantaggi:

  • Dovrai creare più tipi.
  • L'uomo-scimmia berrà per la prima volta un paio d'ore più tardi di quanto potrebbe

Definizione 2. Variabilità unitaria.

Signore, lasciate! La classe dell'alcolista ha una responsabilità unica: bere! E in generale, il concetto di 'responsabilità' è piuttosto vago. Qualcuno è responsabile del destino dell'umanità, mentre qualcun altro è responsabile di sollevare i pinguini caduti al polo.

Consideriamo due implementazioni di alcolista. La prima, quella citata sopra, contiene tre classi: versare, bere e stuzzicare.

La seconda, scritta secondo la metodologia 'Avanti e solo avanti', 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 hanno la stessa responsabilità di 'bere'.

Confusione!

Allora navighiamo in internet e scopriamo un'altra definizione di SRP — Principio di Unica Cambiabilità (Single Changeability Principle).

SCP afferma che 'Un modulo ha uno e solo uno scopo per cambiare'. In altre parole, 'La responsabilità è un motivo per cambiare'.

(Sembra che i ragazzi che hanno inventato la definizione originale fossero sicuri delle capacità telepatiche dell'uomo-scimmia)

Ora tutto torna al suo posto. È possibile modificare separatamente le procedure di versamento, consumo e accompagnamento, mentre nella bevuta possiamo cambiare solo l'ordine e il contenuto delle operazioni, ad esempio spostando l'accompagnamento prima del consumo o aggiungendo la lettura di un brindisi.

Nel piano "Avanti e solo avanti", tutto ciò che può essere modificato viene cambiato solo nel metodo. Act. Questo può essere leggibile ed efficace quando la logica è ridotta e cambia raramente, ma spesso questo si traduce in metodi terribili di 500 righe a ciascuno, con un numero di if superiore a quello necessario per l'entrata della Russia nella NATO.

Definizione 3. Localizzazione delle modifiche.

I bevitori spesso non capiscono perché si siano svegliati in un appartamento sconosciuto o dove sia il loro cellulare. È tempo di aggiungere un log dettagliato.

Iniziamo a loggare il processo di versamento:

class PourOperation: IOperation{
    PourOperation(ILogger log /*....*/){/*...*/}
    //...
    void Do(){
        _log.Log($"Prima di versare con {_hand} e {_bottle}");
        //Logica di versamento ...
        _log.Log($"Dopo aver versato con {_hand} e {_bottle}");
    }
}

Incorporandola in OperazioneVersamento, abbiamo agito saggiamente in termini di responsabilità e incapsulamento, ma ora abbiamo un imbarazzo riguardo al principio di variabilità. Oltre all'operazione stessa, che può variare, anche la registrazione diventa variabile. Dovremo separare 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);
        }
    }
}

Un lettore attento noterà che LogAfter, LogBefore e OnError possono anche cambiare individualmente e, analogamente alle azioni precedenti, creerà tre classi: PourLoggerBefore, PourLoggerAfter e PourErrorLogger.

Ricordando che ci sono tre operazioni per il bevitore, otteniamo nove classi di logging. In totale, il bevitore è composto da 14 (!!!) classi.

Iperbole? Ben poco! Un uomo-scimmia con una granata di decomposizione distruggerà il “riempitore” in decanter, bicchiere, operatori di versamento, servizio di erogazione dell'acqua, modello fisico dell'interazione delle molecole; e nel prossimo trimestre cercherà di districare le dipendenze senza variabili globali. E credetemi — non si fermerà.

Proprio in questo momento molti giungono alla conclusione che il SRP sia una favola dei regni rosa, e se ne vanno a raccontare storie...

… senza mai scoprire l'esistenza della terza definizione di SRP:

«Il principio di responsabilità singola afferma che le cose simili da modificare devono essere conservate in un unico posto«. oppure “Ciò che cambia insieme deve essere conservato in un unico posto

In altre parole, se cambiamo la registrazione dell'operazione, dobbiamo farlo in un solo luogo.

Questo è un punto molto importante — poiché tutte le spiegazioni del SRP fornite precedentemente parlavano di segmentare i tipi, finché essi possono essere suddivisi, ponendo quindi un 'limite superiore' sulla dimensione dell'oggetto, ora parliamo anche di un 'limite inferiore'. In altre parole, Il SRP non solo richiede di 'suddividere finché si può', ma anche di non esagerare — 'non frammentare le cose collegate'. È la grande battaglia tra il rasoio di Occam e l'uomo-scimmia!

Principio di Responsabilità Unica. Non così semplice come sembra

Ora dovrebbe essere più facile per il bevitore. 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 dovessimo aggiungere un quarto tipo di operazione, la registrazione è già pronta. E il codice delle operazioni è pulito e privo di rumore infrastrutturale.

Di conseguenza, abbiamo 5 classi per risolvere il compito del bere:

  • Operazione di versamento
  • Operazione di bevuta
  • Operazione di accompagnamento
  • Logger
  • Fintore per il bevitore

Ognuno di essi è responsabile di una funzionalità specifica e ha una sola ragione per cambiare. Tutte le regole simili per le modifiche sono vicine tra loro.

Esempio della vita reale

Una volta stavamo scrivendo un servizio di registrazione automatica per clienti B2B. E c'era un metodo GOD di 200 righe simile a questo:

  • Vai in 1C e apri un conto
  • Con questo conto, vai al modulo di pagamento e registralo lì
  • Controlla che l'account con quel conto non sia già stato creato nella principale server
  • Crea un nuovo account
  • Risultato della registrazione nel modulo di pagamento e il numero 1c da aggiungere 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. Trasferisci in questo servizio il numero di conto 1c.

E in questo elenco c'erano circa 10 operazioni commerciali con una connessione complessa. L'oggetto del conto era necessario per quasi tutti. L'identificatore del punto e il nome del cliente erano richiesti nella metà delle chiamate.

Dopo un'ora di refactoring, siamo riusciti a separare il codice infrastrutturale e alcune specificità del lavoro con l'account in metodi/classi separate. Il metodo God è diventato più leggero, ma sono rimaste 100 righe di codice che non volevano essere districate.

Solo dopo alcuni giorni è arrivata la comprensione che l'essenza di questo metodo "alleggerito" è l'algoritmo di business. E che la descrizione iniziale del capitolato era piuttosto complessa. E proprio il tentativo di scomporre questo metodo in pezzi violerebbe il SRP, e non il contrario.

Formalismo.

È tempo di lasciare in pace il nostro bevitore. Asciuga le lacrime: torneremo da lui in futuro. Ma ora formalizziamo le conoscenze di questo articolo.

Formalismo 1. Definizione del SRP

  1. Separare gli elementi in modo che ognuno di essi sia responsabile di un singolo aspetto.
  2. La responsabilità si traduce come "motivo di cambiamento". Ciò significa che ogni elemento ha un solo motivo di cambiamento, in termini di logica aziendale.
  3. Le potenziali modifiche alla logica aziendale devono essere localizzate. Gli elementi modificabili simultaneamente devono essere vicini.

Formalismo 2. Criteri necessari per l'autovalutazione.

Non ho trovato criteri sufficienti per il compimento del SRP. Ma ci sono condizioni necessarie:

1) Chiediti: cosa fa questa classe/metodo/modulo/servizio? Devi rispondere a questa domanda con una definizione semplice. (Grazie) Brightori )

suggerimenti

Dopotutto, a volte è molto difficile trovare una definizione semplice.

2) La correzione di un certo bug o l'aggiunta di una nuova funzionalità deve coinvolgere il minor numero possibile di file/classi. Idealmente, uno solo.

suggerimenti

Poiché la responsabilità (per la funzionalità o il bug) è incapsulata in un solo file/classe, sai esattamente dove cercare e cosa modificare. Ad esempio: la funzionalità di modifica dell'output del log delle operazioni richiederà di modificare solo il logger. Non è necessario setacciare tutto il resto del codice.

Un altro esempio è l'aggiunta di un nuovo controllo UI, simile ai precedenti. Se questo ti porta ad aggiungere 10 entità diverse e 15 convertitori diversi, sembra che tu stia "andando oltre".

3) Se più sviluppatori stanno lavorando su diverse funzionalità del tuo progetto, la probabilità di conflitti di merge, cioè la probabilità che lo stesso file/classe venga modificato da più sviluppatori contemporaneamente, è minima.

suggerimenti

Se, aggiungendo una nuova operazione "Versare vodka sotto il tavolo", hai bisogno di toccare il logger, l'operazione di bere e di versare, sembra che le responsabilità siano mal suddivise. Certamente, non è sempre possibile, ma bisogna cercare di ridurre questo indicatore.

4) Quando fai una domanda di chiarimento sulla logica di business (da un sviluppatore o da un manager), vai a cercare informazioni in una sola classe/file e ottieni le informazioni solo da lì.

suggerimenti

Le funzionalità, le regole o gli algoritmi sono scritti in modo compatto, ciascuno in un unico posto, e non sparsi per tutto il codice con flag.

5) I nomi sono chiari.

suggerimenti

La nostra classe o metodo è responsabile di un'unica cosa, e la responsabilità è riflessa nel suo nome.

AllManagersManagerService è probabilmente un God-class.
LocalPayment — probabilmente no

Formalismo 3. Metodo di sviluppo «Occam-first».

All'inizio della progettazione, l'homo sapiens non conosce e non percepisce tutte le sfumature del problema da risolvere e può commettere errori. Gli errori possono avvenire in vari modi:

  • Creare oggetti troppo grandi, unendo diverse responsabilità
  • Frantumare, separando una responsabilità unica in molti tipi diversi
  • Definire erroneamente i confini della responsabilità

È importante ricordare la regola: «è meglio sbagliare in grande», o «se non sei sicuro, non frantumare». Se, ad esempio, la tua classe raccoglie due responsabilità — allora rimane comprensibile e può essere divisa in due con modifiche minime al codice client. Creare un bicchiere da schegge di vetro è generalmente più complesso a causa di un contesto sfocato su più file e della mancanza delle dipendenze necessarie nel codice client.

È ora di concludere

L'ambito di applicazione del SRP non si limita a OOP e SOLID. Si applica a metodi, funzioni, classi, moduli, microservizi e servizi. È adatto sia per lo sviluppo “figaks-figaks-e-in-prod” che per “rocket-science”, rendendo ovunque il mondo un po' migliore. Se ci pensi, questo è quasi un principio fondamentale di tutto l'ingegneria. L'ingegneria meccanica, i sistemi di controllo, e in generale tutti i sistemi complessi sono costruiti da componenti, e la "non-frammentazione" priva i costruttori di flessibilità, l'"eccessiva frammentazione" - di efficienza, e i confini errati - di ragione e tranquillità.

Principio di Responsabilità Unica. Non così semplice come sembra

Il SRP non è inventato dalla natura e non fa parte delle scienze esatte. Nasce dalle nostre limitazioni biologiche e psicologiche. È semplicemente un modo per controllare e sviluppare sistemi complessi con l'aiuto del cervello umano-primato. Ci insegna come decomporre un sistema. La formulazione originale richiedeva una notevole abilità telepatica, ma spero che questo articolo abbia un po' dissipato l'illusione.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster