Principio della singola responsabilità: come posso evitare la frammentazione del codice?

54

Sto lavorando a un team in cui il leader del team è un sostenitore virulento dei principi di sviluppo SOLID. Tuttavia, gli manca un sacco di esperienza nell'ottenere software complesso fuori dalla porta.

Abbiamo una situazione in cui ha applicato SRP a quello che era già un codice piuttosto complesso, che ora è diventato molto frammentato, difficile da capire e correggere.

Ora abbiamo un problema non solo con la frammentazione del codice, ma anche con l'incapsulamento, poiché i metodi all'interno di una classe che potrebbe essere stata privata o protetta sono stati giudicati come una "ragione per cambiare" e sono stati estratti in classi pubbliche o interne e interfacce che non sono in linea con gli obiettivi di incapsulamento dell'applicazione.

Abbiamo alcuni costruttori di classi che prendono più di 20 parametri di interfaccia, quindi la nostra registrazione e risoluzione IoC sta diventando un mostro a sé stante.

Voglio sapere se esiste un approccio "refactoring away from SRP" che potremmo usare per risolvere alcuni di questi problemi. Ho letto che non viola SOLID se creo un certo numero di classi vuote a grana grossa che "avvolgono" un certo numero di classi strettamente correlate per fornire un punto di accesso unico alla somma delle loro funzionalità (ad esempio mimando un implementazione eccessiva della classe SRP).

A parte questo, non riesco a pensare a una soluzione che ci consenta di proseguire pragmaticamente con i nostri sforzi di sviluppo, mantenendo tutti felici.

Qualche suggerimento?

    
posta Dean Chalk 29.05.2012 - 12:19
fonte

7 risposte

80

Se la tua classe ha 20 parametri nel costruttore, non sembra che la tua squadra sappia cosa sia SRP. Se hai una classe che fa solo una cosa, come ha 20 dipendenze? È come fare una battuta di pesca e portare con sé una canna da pesca, una scatola di attrezzi, un rifornimento di quilt, una palla da bowling, un nunchaku, un lanciafiamme, ecc ... Se hai bisogno di tutto per andare a pescare, non vai a pescare.

Detto questo, SRP, come la maggior parte dei principi là fuori, può essere sovra-applicato. Se crei una nuova classe per incrementare interi, allora sì, questa potrebbe essere una singola responsabilità, ma dai. È ridicolo. Tendiamo a dimenticare che cose come i principi SOLID ci sono per uno scopo. SOLID è un mezzo per un fine, non un fine in sé. La fine è manutenibilità . Se hai intenzione di ottenere quel granulare con il Principio di Responsabilità Unico, è un indicatore che lo zelo per SOLID ha accecato la squadra verso l'obiettivo di SOLID.

Quindi, suppongo che quello che sto dicendo sia ... L'SRP non è un tuo problema. Si tratta di un fraintendimento dell'SRP o di un'applicazione incredibilmente dettagliata. Cerca di convincere la tua squadra a mantenere la cosa principale la cosa principale. E la cosa principale è la mantenibilità.

Modifica

Invita le persone a progettare moduli in modo da favorire la facilità d'uso. Pensa a ogni classe come a una mini API. Pensa prima, "Come mi piacerebbe usare questa classe", e poi implementarla. Non pensare solo a "cosa deve fare questa lezione". L'SRP ha una grande tendenza a rendere le classi più difficili da usare, se non ci si preoccupa molto dell'usabilità.

EDIT 2

Se stai cercando suggerimenti per il refactoring, puoi iniziare a fare ciò che hai suggerito: creare classi più grosse per avvolgerne altre. Assicurati che la classe a grana grossa aderisca ancora allo SRP , ma a un livello superiore. Quindi hai due alternative:

  1. Se le classi a grana più fine non sono più utilizzate altrove nel sistema, puoi gradualmente estendere la loro implementazione nella classe a grana grossa ed eliminarle.
  2. Lascia le classi più fini da solo. Forse sono stati progettati bene e hai solo bisogno del wrapper per renderli più facili da usare. Sospetto che questo sia il caso di gran parte del tuo progetto.

Quando hai finito di refactoring (ma prima di impegnarti nel repository), rivedi il tuo lavoro e chiediti se il tuo refactoring fosse in realtà un miglioramento della manutenibilità e della facilità d'uso.

    
risposta data 30.05.2012 - 05:28
fonte
29

Penso che sia nel Refactoring di Martin Fowler che una volta ho letto una contro-regola per SRP, definendo dove sta andando troppo lontano. C'è una seconda domanda, importante quanto "ogni classe ha una sola ragione per cambiare?" e cioè "ogni cambiamento interessa solo una classe?"

Se la risposta alla prima domanda è, in ogni caso, "sì", ma la seconda domanda è "non ancora chiusa", allora devi guardare di nuovo a come stai implementando SRP.

Ad esempio, se aggiungere un campo a una tabella significa che devi modificare un DTO e una classe di validatore e una classe di persistenza e un oggetto modello di vista e così via hai creato un problema. Forse dovresti ripensare a come hai implementato SRP.

Forse hai detto che aggiungere un campo è la ragione per cambiare l'oggetto Cliente, ma cambiare il livello di persistenza (ad esempio da un file XML a un database) è un altro motivo per cambiare l'oggetto Cliente. Quindi decidi di creare anche un oggetto CustomerPersistence. Ma se lo fai in modo tale che aggiungere un campo ANCORA richiede una modifica all'oggetto CustomerPersisitence allora qual è stato il punto? Hai ancora un oggetto con due motivi per cambiare: non è più un Cliente.

Tuttavia, se si introduce un ORM, è possibile che le classi funzionino in modo tale che se si aggiunge un campo al DTO, esso cambierà automaticamente l'SQL utilizzato per leggere quei dati. Allora hai una buona ragione per separare le due preoccupazioni.

In sintesi, ecco cosa tendo a fare: se c'è un equilibrio approssimativo tra il numero di volte che dico "no, c'è più di un motivo per cambiare questo oggetto" e il numero di volte che dico "no, questo cambiamento riguarderà più di un oggetto ", quindi penso di avere il giusto equilibrio tra SRP e frammentazione. Ma se entrambi sono ancora alti allora comincio a chiedermi se c'è un modo diverso per separare le preoccupazioni.

    
risposta data 30.05.2012 - 05:58
fonte
21

Solo perché un sistema è complesso non significa che devi renderlo complicato . Se hai una classe con troppe dipendenze (o collaboratori) come questa:

public class MyAwesomeClass {
    public class MyAwesomeClass(IDependency1 _d1, IDependency2 _d2, ... , IDependency20 _d20) {
      // Assign it all
    }
}

... poi è diventato troppo complicato e non stai seguendo veramente SRP , vero? Scommetto che se scrivessi cosa MyAwesomeClass fa su una scheda CRC non andrebbe bene su una scheda indice o devi scrivere in lettere minuscole illeggibili.

Quello che hai qui è che i tuoi ragazzi hanno solo seguito il principio di segregazione dell'interfaccia e potrebbero averlo portato ad un estremo ma questa è tutta un'altra storia. Si potrebbe sostenere che le dipendenze sono oggetti di dominio (cosa che accade), tuttavia avere una classe che gestisce 20 oggetti di dominio allo stesso tempo lo sta allungando un po 'troppo.

TDD ti fornirà un buon indicatore di quanto fa una classe. Mettere senza mezzi termini; se un metodo di test ha un codice di installazione che richiede sempre una scrittura (anche se si refactoring i test), il MyAwesomeClass probabilmente ha troppe cose da fare.

Quindi, come risolvi questo enigma? Sposti le responsabilità ad altre classi. Ci sono alcuni passaggi che puoi seguire in una classe che presenta questo problema:

  1. Identifica tutte le azioni (o responsabilità) che la tua classe fa con le sue dipendenze.
  2. Raggruppa le azioni in base a dipendenze strettamente correlate.
  3. Redelegate! I.e. refactoring ciascuna delle azioni identificate a nuove o (più importante) altre classi.

Un esempio astratto sulle responsabilità di refactoring

Lascia che C sia una classe che ha diverse dipendenze D1 , D2 , D3 , D4 che devi rifattorizzare per usare meno. Quando identifichiamo quali metodi che C chiama sulle dipendenze, possiamo fare un semplice elenco di essi:

  • D1 - performA(D2) , performB()
  • D2 - performD(D1)
  • D3 - performE()
  • D4 - performF(D3)

Guardando l'elenco possiamo vedere che D1 e D2 sono correlati l'uno all'altro in quanto la classe li richiede in qualche modo. Possiamo anche vedere che D4 ha bisogno di D3 . Quindi abbiamo due gruppi:

  • Group 1 - D1 < - > %codice%
  • D2 - Group 2 - > %codice%

I raggruppamenti sono un indicatore del fatto che la classe ora ha due responsabilità.

  1. D4 - Uno per gestire i due oggetti chiamanti che hanno bisogno l'uno dell'altro. Forse puoi lasciare che la tua classe D3 elimini la necessità di gestire entrambe le dipendenze e lasciare che uno di loro gestisca quelle chiamate. In questo raggruppamento, è ovvio che Group 1 potrebbe avere un riferimento a C .
  2. D1 - L'altra responsabilità ha bisogno di un oggetto per chiamarne un altro. Impossibile D2 gestire Group 2 invece della tua classe? Quindi probabilmente possiamo eliminare D4 dalla classe D3 lasciando che D3 faccia invece le chiamate.

Non prendere la mia risposta come se fossi scolpito nella pietra, dato che l'esempio è molto astratto e fa molte supposizioni. Sono abbastanza sicuro che ci siano altri modi per refactoring, ma almeno i passaggi potrebbero aiutarti a ottenere una sorta di processo per spostare le responsabilità in giro invece di dividere le classi.

Modifica:

Tra i commenti @Emmad Karem dice:

"If your class has 20 parameters in the constructor, it doesn't sound like your team quite knows what SRP is. If you have a class that does only one thing, how does it have 20 dependencies? " - I think that If you have a Customer class, it is not strange to have 20 parameters in the constructor.

È vero che gli oggetti DAO tendono ad avere molti parametri, che devi impostare nel costruttore e i parametri sono in genere semplici tipi come stringa. Tuttavia, nell'esempio di una classe C , puoi comunque raggruppare le sue proprietà all'interno di altre classi per semplificare le cose. Ad esempio avere una classe D4 con strade e una classe Customer che contiene il codice di avviamento postale e gestirà la logica aziendale come la convalida dei dati:

public class Address {
    private String street1;
    //...

    private Zipcode zipcode;

    // easy to extend
    public bool isValid() {
        return zipcode.isValid();
    }
}

public class Zipcode {
    private string zipcode;
    public bool isValid() {
        // return regex match that zipcode contains numbers
    }
}

Questa cosa è discussa ulteriormente nel post del blog "Mai, mai, mai usare String in Java (o almeno spesso)" . In alternativa all'utilizzo di costruttori o metodi statici per semplificare la creazione degli oggetti secondari, è possibile utilizzare un modello di generatore di fluidi .

    
risposta data 30.05.2012 - 13:43
fonte
3

Sono d'accordo con tutte le risposte sull'SRP e su come può essere portato troppo lontano. Nel tuo post hai menzionato che a causa di "refactoring" per aderire a SRP hai trovato la rottura o l'alterazione dell'encapsulation. L'unica cosa che ha funzionato per me è di attenersi sempre alle basi e fare esattamente ciò che è necessario per raggiungere un fine.

Quando si lavora con i sistemi Legacy, l '"entusiasmo" per sistemare tutto per renderlo migliore di solito è piuttosto alto nei Team Lead, specialmente quelli che sono nuovi in quel ruolo. SOLID, semplicemente non ha SRP - Questo è solo il S. Assicurati che se stai seguendo SOLID, non dimentichi anche l'OLID.

Sto lavorando su un sistema Legacy in questo momento e abbiamo iniziato a seguire un percorso simile all'inizio. Ciò che ha funzionato per noi è stata una decisione collettiva del team per sfruttare al meglio entrambi i mondi: SOLID e K.I.S.S (Keep It Simple Stupid). Abbiamo discusso collettivamente di importanti cambiamenti nella struttura del codice e applicato il buon senso nell'applicazione di vari principi di sviluppo. Sono grandi come linee guida non "Leggi sullo sviluppo S / W". Il team non riguarda solo il Team Lead: riguarda tutti gli sviluppatori del team. La cosa che ha sempre funzionato per me è far entrare tutti in una stanza e trovare una serie di linee guida condivise che il tuo intero team è d'accordo a seguire.

Per quanto riguarda come correggere la situazione attuale, se si utilizza un VCS e non sono state aggiunte troppe nuove funzionalità all'applicazione, è sempre possibile tornare a una versione di codice che l'intero team ritiene sia comprensibile, leggibile e mantenibile. Sì! Ti sto chiedendo di buttar via il lavoro e ricominciare da capo. È meglio che cercare di "sistemare" qualcosa che è stato rotto e spostarlo di nuovo in qualcosa che esisteva già.

    
risposta data 01.06.2012 - 10:38
fonte
3

La risposta è manutenibilità e chiarezza del codice sopra ogni altra cosa. Per me questo significa scrivere less code , non di più. Meno astrazioni, meno interfacce, meno opzioni, meno parametri.

Ogni volta che valuto una ristrutturazione del codice, o aggiungendo una nuova funzione, penso a quanto quantità di piastra termica sarà necessaria rispetto alla logica effettiva. Se la risposta è superiore al 50%, probabilmente significa che ci sto pensando troppo.

Oltre a SRP, ci sono molti altri stili di sviluppo. Nel tuo caso i suoni come YAGNI sono decisamente carenti.

    
risposta data 08.06.2012 - 22:21
fonte
3

Molte delle risposte qui sono davvero buone, ma si concentrano sul lato tecnico di questo problema. Aggiungerò semplicemente che sembra che i tentativi dello sviluppatore di seguire il suono SRP siano come se violassero lo SRP.

Puoi vedere il blog di Bob qui su questa situazione , ma sostiene che se una responsabilità viene spalmata su più classi, allora l'SRP della responsabilità viene violato perché quelle classi cambiano in parallelo. Sospetto che il tuo dev apprezzerebbe molto il design in cima al blog di Bob, e potrebbe essere un po 'deluso vederlo squarciato. In particolare perché viola il "Common Closure Principle" - le cose che cambiano insieme rimangono insieme.

Ricorda che l'SRP si riferisce alla "ragione del cambiamento" e non "fa una cosa", e che non devi preoccuparti di quel motivo di cambiamento finché non si verifica effettivamente un cambiamento. Il secondo ragazzo paga per l'astrazione.

Ora c'è il secondo problema: il "sostenitore virulento dello sviluppo SOLID". Di sicuro non sembra che tu abbia un ottimo rapporto con questo sviluppatore, quindi qualsiasi tentativo di convincerlo / a dei problemi nella base di codice viene messo a tacere. Avrai bisogno di riparare la relazione in modo da poter avere una vera discussione dei problemi. Quello che vorrei raccomandare è la birra.

No seriamente - se non bevi vai in un bar. Esci dall'ufficio e da qualche parte rilassato, dove puoi parlare di questa roba in modo informale. Piuttosto che cercare di vincere una discussione durante una riunione, cosa che non vorresti, fare una discussione in qualche modo divertente. Cerca di riconoscere che questo dev, che ti sta facendo impazzire, è un vero e proprio essere umano che sta cercando di far uscire il software "fuori dalla porta" e non vuole spedire schifo. Dal momento che probabilmente condividi questo terreno comune, puoi iniziare a discutere su come migliorare il design mentre si sta ancora conformando all'SRP.

Se riesci entrambi a riconoscere che l'SRP è una buona cosa, che interpreti semplicemente gli aspetti in modo diverso, puoi probabilmente iniziare ad avere conversazioni produttive.

    
risposta data 19.01.2016 - 14:27
fonte
-1

Sono d'accordo con la decisione del tuo team lead [aggiornamento = 2012.05.31] che l'SRP è generalmente una buona idea. Ma sono completamente d'accordo sul commento di @ Spoike che un costruttore con 20 argomenti di interfaccia è di gran lunga troppo [/ update]:

L'introduzione di SRP con IoC sposta la complessità da una "multi-responsabile-classe" a molte classi srp e un'inizializzazione molto più complicata a vantaggio di

  • easy unit-testability / tdd (testare una classe srp in isolamento alla volta)
  • ma a costo di
    • un'inizializzazione e integrazione del codice molto più difficile e
    • più problemi di debugging
    • frammentazione (= distribuzione del codice su più file / directory)

Temo di non poter ridurre la frammentazione del codice senza sacrificare srp.

Ma puoi "alleviare il dolore" della codeinitializzazione implementando una classe di zucchero sintattica che nasconde la complessità dell'inizializzazione in un costruttore.

   class MySrpClass {
      MySrpClass(Interface1 parm1, Interface2 param2, .... Interface20 param2) {
      }
   } 

   class MySyntaxSugarClass : MySrpClass {
      MySyntaxSugarClass() {
         super(new MyInterface1Implementation(), new MyImpl2(), ....)
      }
   }
    
risposta data 29.05.2012 - 14:08
fonte

Leggi altre domande sui tag