Principio della singola responsabilità - Come posso evitare la frammentazione del codice?


57

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

Abbiamo una situazione in cui ha applicato SRP a quella che era già una base di codice piuttosto complessa, che ora è diventata molto frammentata e difficile da capire e da debug.

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 potrebbero essere stati privati ​​o protetti sono stati giudicati come un "motivo per cambiare" e sono stati estratti in classi e interfacce pubbliche o interne che non è in linea con gli obiettivi di incapsulamento dell'applicazione.

Abbiamo alcuni costruttori di classe che accettano oltre 20 parametri di interfaccia, quindi la nostra registrazione e risoluzione IoC sta diventando un mostro a sé stante.

Voglio sapere se esiste un approccio "refactor away from SRP" che potremmo usare per aiutare a risolvere alcuni di questi problemi. Ho letto che non viola SOLID se creo un numero di classi vuote a grana grossa che "avvolgono" un numero di classi strettamente correlate per fornire un unico punto di accesso alla somma delle loro funzionalità (ovvero imitando un valore inferiore implementazione di classe eccessivamente SRP).

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

Eventuali suggerimenti ?


18
Questa è solo la mia opinione, ma penso che ci sia un'altra regola, che viene facilmente dimenticata sotto la pila di vari acronimi: "Principio del senso comune". Quando una "soluzione" crea più problemi che risolve davvero, allora qualcosa non va. La mia opinione è che se un problema è complesso, ma è racchiuso in una classe che si occupa delle sue complessità ed è ancora relativamente facile da eseguire il debug - lo sto lasciando da solo. Generalmente la tua idea di "wrapper" mi sembra valida, ma lascerò la risposta a qualcuno più esperto.
Patryk iewiek,

6
Per quanto riguarda la "ragione del cambiamento", non è necessario speculare prematuramente su tutti i motivi. Attendi fino a quando non devi effettivamente cambiarlo, quindi vedi cosa si può fare per rendere più facile questo tipo di cambiamento in futuro.

62
Una classe con 20 parametri del costruttore non suona molto SRP per me!
MattDavey,

1
Scrivi "... Registrazione e risoluzione IoC ..."; sembra che tu (o il tuo capo squadra) pensi che "IoC" e "iniezione di dipendenza" (DI) siano la stessa cosa, il che non è vero. DI è un mezzo per raggiungere l'IoC, ma certamente non l'unico. Dovresti analizzare attentamente perché vuoi fare l'IoC; se è perché si desidera scrivere unit test, allora si potrebbe anche provare a usare il modello di localizzazione del servizio o semplicemente l'interfaccia di classi ( ISomething). IMHO, questi approcci sono molto più facili da gestire rispetto all'iniezione di dipendenza e danno come risultato un codice più leggibile.

2
qualsiasi risposta data qui sarebbe nel vuoto; dovremmo vedere il codice per dare una risposta specifica. 20 parametri in un costruttore? beh, potresti perdere un oggetto ... o potrebbero essere tutti validi; o potrebbero appartenere a un file di configurazione, o potrebbero appartenere a una classe DI, oppure ... I sintomi sembrano certamente sospetti, ma come la maggior parte delle cose in CS, "dipende" ...
Steven A. Lowe

Risposte:


85

Se la tua classe ha 20 parametri nel costruttore, non sembra che il tuo team sappia esattamente cos'è SRP. Se hai una classe che fa solo una cosa, come ha 20 dipendenze? È come andare in una battuta di pesca e portare con sé una canna da pesca, una scatola per l'attrezzatura, forniture per imbottitura, palla da bowling, nunchucks, lanciafiamme, ecc .... Se hai bisogno di tutto ciò per andare a pescare, non stai solo andando a pescare.

Detto questo, l'SRP, come la maggior parte dei principi, può essere applicato in modo eccessivo. Se crei una nuova classe per incrementare numeri interi, allora sì, questa potrebbe essere una singola responsabilità, ma dai. È ridicolo. Tendiamo a dimenticare che cose come i principi SOLID sono lì per uno scopo. SOLID è un mezzo per raggiungere un fine, non un fine in sé. La fine è la manutenibilità . Se hai intenzione di ottenere questo granulare con il principio di responsabilità singola, è 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. È un fraintendimento dell'SRP o un'applicazione incredibilmente granulare di esso. Cerca di convincere la tua squadra a mantenere la cosa principale come cosa principale. E la cosa principale è la manutenibilità.

MODIFICARE

Fai in modo che le persone progettino i moduli in un modo che incoraggi la facilità d'uso. Pensa a ogni classe come a una mini API. Pensa innanzitutto, "Come mi piacerebbe usare questa classe", quindi implementarla. Non pensare semplicemente "Cosa deve fare questa classe". L'SRP ha una grande tendenza a rendere le classi più difficili da usare, se non si considera molto l'usabilità.

MODIFICA 2

Se stai cercando consigli sul refactoring, puoi iniziare a fare ciò che hai suggerito: creare classi più grossolane per avvolgerne molte altre. Assicurarsi che la classe a grana più grossa aderisca ancora all'SRP , ma a un livello superiore. Quindi hai due alternative:

  1. Se le classi a grana fine non vengono più utilizzate altrove nel sistema, è possibile trascinarne gradualmente l'implementazione nella classe a grana più grossa ed eliminarle.
  2. Lascia in pace le classi più fini. Forse sono stati progettati bene e hai solo bisogno dell'involucro per renderli più facili da usare. Sospetto che questo sia il caso di gran parte del tuo progetto.

Quando hai terminato il refactoring (ma prima di impegnarti nel repository), rivedi il tuo lavoro e chiediti se il refactoring è stato effettivamente un miglioramento della manutenibilità e della facilità d'uso.


2
Un modo alternativo per indurre le persone a pensare alla progettazione di classi: lascia che scrivano carte CRC (Nome classe, Responsabilità, Collaboratori) . Se una classe ha troppi collaboratori o responsabilità, molto probabilmente non è abbastanza SRP. In altre parole, tutto il testo deve adattarsi alla scheda indice, altrimenti sta facendo troppo.
Spoike,

18
So a cosa serve il lanciafiamme, ma come diamine si pesca con un'asta?
R. Martinho Fernandes,

13
+1 SOLID è un mezzo per raggiungere un fine, non un fine in sé.
B Sette,

1
+1: Ho già sostenuto che cose come "La legge di Demetra" sono errate, dovrebbe essere "La linea guida di Demetra". Queste cose dovrebbero funzionare per te, non dovresti lavorare per loro.
Binary Worrier,

2
@EmmadKareem: è vero che gli oggetti DAO dovrebbero avere diverse proprietà. Ma ancora una volta, ci sono diverse cose che puoi raggruppare in qualcosa di semplice come una Customerclasse e avere un codice più gestibile. Vedi esempi qui: codemonkeyism.com/…
Spoike

33

Penso che sia nel refactoring di Martin Fowler che una volta ho letto una contro-regola per SRP, che definisce dove sta andando troppo lontano. C'è una seconda domanda, importante quanto "ogni classe ha solo un motivo per cambiare?" e cioè "ogni modifica influisce solo su una classe?"

Se la risposta alla prima domanda è, in ogni caso, "sì", ma la seconda domanda è "nemmeno vicina", allora è necessario esaminare nuovamente come si sta implementando SRP.

Ad esempio, se l'aggiunta di un campo a una tabella significa che è necessario modificare un DTO e una classe di validazione e una classe di persistenza e un oggetto modello vista e così via, è stato creato un problema. Forse dovresti ripensare a come hai implementato SRP.

Forse hai detto che l'aggiunta di un campo è il motivo per cambiare l'oggetto Cliente, ma cambiare il livello di persistenza (diciamo 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 l'aggiunta di un campo STILL richieda una modifica all'oggetto CustomerPersisitence, qual è il punto? Hai ancora un oggetto con due motivi per cambiare: semplicemente 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, cambierà automaticamente l'SQL utilizzato per leggere quei dati. Quindi hai buone ragioni per separare le due preoccupazioni.

In breve, 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 lo farà influenzano più di un oggetto ", quindi penso di avere il giusto equilibrio tra SRP e frammentazione. Ma se entrambi sono ancora alti, allora mi chiedo se c'è un modo diverso di separare le preoccupazioni.


+1 per "ogni modifica riguarda solo una classe?"
dj18

Un problema correlato di cui non ho mai discusso è che se le attività legate a un'entità logica vengono frammentate tra classi diverse, potrebbe essere necessario che il codice contenga riferimenti a più oggetti distinti che sono tutti collegati alla stessa entità. Si consideri, ad esempio, un forno con le funzioni "SetHeaterOutput" e "MeasureTemperature". Se il forno fosse rappresentato da oggetti indipendenti HeaterControl e TemperatureSensor, allora nulla impedirebbe a un oggetto TemperatureFeedbackSystem di contenere un riferimento al riscaldatore di un forno e un sensore di temperatura del forno diverso.
Supercat,

1
Se invece tali funzioni fossero combinate in un'interfaccia IKiln, che è stata implementata da un oggetto Kiln, il TemperatureFeedbackSystem dovrebbe contenere solo un singolo riferimento IKiln. Se fosse necessario utilizzare un forno con un sensore di temperatura aftermarket indipendente, si potrebbe usare un oggetto CompositeKiln il cui costruttore ha accettato un IHeaterControl e un ITemperatureSensor e li ha usati per implementare IKiln, ma tale composizione voluta deliberata sarebbe facilmente riconoscibile nel codice.
supercat,

24

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

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

... allora è diventato troppo complicato e non stai davvero seguendo SRP , vero? Scommetto che se annotassi ciò che MyAwesomeClassfa su una scheda CRC non si adatterebbe su una scheda indice o dovresti scrivere in lettere illeggibili davvero minuscole.

Quello che hai qui è che i tuoi ragazzi hanno seguito solo il Principio di segregazione dell'interfaccia invece e potrebbero averlo portato all'estremo ma questa è tutta un'altra storia. Si potrebbe obiettare che le dipendenze sono oggetti di dominio (cosa che succede), tuttavia avere una classe che gestisce 20 oggetti di dominio contemporaneamente lo sta allungando un po 'troppo.

TDD ti fornirà un buon indicatore di quanto fa una classe. Senza mezzi termini; se un metodo di test ha un codice di installazione che richiede un'eternità per scrivere (anche se si esegue il refactoring dei test), allora MyAwesomeClassprobabilmente ci sono troppe cose da fare.

Quindi, come risolvi questo enigma? Sposta le responsabilità in altre classi. Esistono alcuni passaggi che è possibile eseguire su una classe che presenta questo problema:

  1. Identifica tutte le azioni (o responsabilità) che la tua classe fa con le sue dipendenze.
  2. Raggruppare le azioni in base a dipendenze strettamente correlate.
  3. Redelegate! Vale a dire refactoring ciascuna delle azioni identificate a nuove o (soprattutto) altre classi.

Un esempio astratto di responsabilità di refactoring

Lasciate Cessere una classe che ha diverse dipendenze D1, D2, D3, D4che è necessario refactoring di usare meno. Quando identifichiamo quali metodi che fanno Cappello alle dipendenze possiamo fare un semplice elenco:

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

Guardando l'elenco possiamo vederlo D1e D2siamo in relazione tra loro in quanto la classe ne ha bisogno insieme in qualche modo. Possiamo anche vedere che ha D4bisogno D3. Quindi abbiamo due raggruppamenti:

  • Group 1- D1<->D2
  • Group 2- D4->D3

I raggruppamenti indicano che la classe ha ora due responsabilità.

  1. Group 1- Uno per gestire i due oggetti chiamanti che hanno bisogno l'uno dell'altro. Forse puoi permettere alla tua classe di Celiminare la necessità di gestire entrambe le dipendenze e lasciare invece una di esse che gestisce quelle chiamate. In questo gruppo, è ovvio che D1potrebbe avere un riferimento D2.
  2. Group 2- L'altra responsabilità ha bisogno di un oggetto per chiamarne un altro. Non riesci a D4gestire al D3posto della tua classe? Quindi probabilmente possiamo eliminare D3dalla classe Clasciando invece D4effettuare le chiamate.

Non prendere la mia risposta incastonata nella pietra, poiché l'esempio è molto astratto e fa molte ipotesi. Sono abbastanza sicuro che ci sono altri modi per riformattare questo, ma almeno i passaggi potrebbero aiutarti a ottenere un qualche tipo di processo per spostare le responsabilità invece di dividere le classi.


Modificare:

Tra i commenti di @Emmad Karem afferma:

"Se la tua classe ha 20 parametri nel costruttore, non sembra che il tuo team sappia esattamente cosa sia SRP. Se hai una classe che fa solo una cosa, come può avere 20 dipendenze?" - Penso che se tu avere una classe Customer, non è strano avere 20 parametri nel costruttore.

È vero che gli oggetti DAO tendono ad avere molti parametri, che devi impostare nel tuo costruttore, e i parametri sono di solito tipi semplici come stringa. Tuttavia, nell'esempio di una Customerclasse, puoi ancora raggruppare le sue proprietà all'interno di altre classi per semplificare le cose. Ad esempio avere una Addressclasse con strade e una Zipcodeclasse che contiene il codice postale e gestirà anche 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 di oggetti secondari, è possibile utilizzare un modello di generatore di fluido .


+1: ottima risposta! Il raggruppamento è l'IMO un meccanismo molto potente perché è possibile applicare il raggruppamento in modo ricorsivo. Parlando in modo molto approssimativo, con n livelli di astrazione è possibile organizzare 2 ^ n elementi.
Giorgio,

+1: I tuoi primi paragrafi riassumono esattamente ciò che la mia squadra sta affrontando. Gli "oggetti business" che in realtà sono oggetti di servizio e il codice di installazione di unit test che è ansioso di scrivere. Sapevo che avevamo un problema quando le chiamate del nostro livello di servizio contenevano una riga di codice; una chiamata a un metodo di livello aziendale.
Uomo,

3

Concordo con tutte le risposte su SRP e su come può essere portato troppo lontano. Nel tuo post dici che a causa del "refactoring eccessivo" per aderire a SRP hai trovato l'incapsulamento che si rompe o viene modificato. L'unica cosa che ha funzionato per me è attenersi sempre alle basi e fare esattamente ciò che è necessario per raggiungere un fine.

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

Sto lavorando su un sistema Legacy in questo momento e abbiamo iniziato a percorrere un percorso simile all'inizio. Ciò che ha funzionato per noi è stata la decisione di un gruppo collettivo di trarre il meglio da entrambi i mondi: SOLID e KISS (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 ottimi come linee guida e non "Leggi di sviluppo S / W". Il team non è solo responsabile del team, ma riguarda tutti gli sviluppatori del team. La cosa che ha sempre funzionato per me è far entrare tutti in una stanza e inventare una serie di linee guida condivise che tutta la tua squadra accetta di seguire.

Per quanto riguarda come risolvere la situazione attuale, se usi un VCS e non hai aggiunto troppe nuove funzionalità alla tua applicazione, puoi sempre tornare a una versione di codice che l'intero team ritiene comprensibile, leggibile e gestibile. Sì! Ti sto chiedendo di buttare via il lavoro e ricominciare da zero. È meglio che provare a "riparare" qualcosa che è stato rotto e riportarlo a qualcosa che già esisteva.


3

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

Ogni volta che valuto una ristrutturazione del codice o aggiungo una nuova funzionalità, penso a quanta caldaia sarà richiesta rispetto alla logica effettiva. Se la risposta è superiore al 50%, probabilmente significa che sto pensando troppo.

Oltre a SRP, ci sono molti altri stili di sviluppo. Nel tuo caso sembra che manchi YAGNI.


3

Molte delle risposte qui sono davvero buone, ma sono focalizzate sul lato tecnico di questo problema. Aggiungerò semplicemente che sembra che i tentativi dello sviluppatore di seguire il suono dell'SRP violino effettivamente l'SRP.

Puoi vedere il blog di Bob qui su questa situazione, ma sostiene che se una responsabilità viene spalmata su più classi, la responsabilità SRP viene violata perché tali classi cambiano in parallelo. Sospetto che il tuo sviluppatore vorrebbe davvero il design nella parte superiore del blog di Bob e potrebbe essere un po 'deluso nel vederlo fatto a pezzi. In particolare perché viola il "Principio di chiusura comune" - le cose che cambiano insieme rimangono insieme.

Ricorda che l'SRP si riferisce a "motivo del cambiamento" e non a "fare una cosa", e che non devi preoccuparti di quel motivo del cambiamento fino a quando non si verifica effettivamente un cambiamento. Il secondo ragazzo paga per l'astrazione.

Ora c'è il secondo problema: il "sostenitore virulento dello sviluppo SOLID". Sicuramente non sembra che tu abbia un ottimo rapporto con questo sviluppatore, quindi qualsiasi tentativo di convincerlo dei problemi nella base di codice è bloccato. Dovrai riparare la relazione in modo da poter avere una vera discussione dei problemi. Quello che consiglierei è la birra.

No sul serio: se non bevi, vai in un bar. Esci dall'ufficio e rilassati, dove puoi parlare di queste cose in modo informale. Invece di provare a vincere una discussione in una riunione, cosa che non vuoi, fai una discussione in qualche posto divertente. Cerca di riconoscere che questo sviluppatore, che ti sta facendo impazzire, è un vero umano funzionante che sta cercando di ottenere il software "fuori dalla porta" e non vuole spedire cazzate. Dato che probabilmente condividi quel terreno comune, puoi iniziare a discutere su come migliorare il design pur rimanendo conforme all'SRP.

Se entrambi riconoscete che l'SRP è una buona cosa, che interpretate semplicemente gli aspetti in modo diverso, probabilmente potete iniziare ad avere conversazioni produttive.


-1

Sono d'accordo con la tua decisione del team [aggiornamento = 2012.05.31] che SRP è generalmente una buona idea. Ma sono pienamente d'accordo con il commento di @ Spoike che un costruttore con 20 argomenti di interfaccia è molto lontano. [/ Update]:

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

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

Temo che non puoi ridurre la frammentazione del codice senza sacrificare srp.

Ma puoi "alleviare il dolore" della codeinizializzazione implementando una classe di zucchero sintattico 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(), ....)
      }
   }

2
Credo che 20 interfacce siano un indicatore che la classe ha troppo da fare. Cioè ci sono 20 ragioni per cambiare, il che è praticamente una violazione di SRP. Solo perché il sistema è complesso non significa che debba essere complicato.
Spoike,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.