Quali sono i modi pratici per implementare l'SRP?


11

Semplicemente quali sono le tecniche pratiche che le persone usano per verificare se una classe viola il singolo principio di responsabilità?

So che una classe dovrebbe avere solo una ragione per cambiare, ma a quella frase manca un modo pratico per implementarla davvero.

L'unico modo che ho trovato è usare la frase "Il ......... dovrebbe ......... stesso". dove il primo spazio è il nome della classe e il successivo è il nome del metodo (responsabilità).

Tuttavia, a volte è difficile capire se una responsabilità viola davvero l'SRP.

Esistono altri modi per verificare l'SRP?

Nota:

La domanda non riguarda il significato dell'SRP, ma piuttosto una metodologia pratica o una serie di passaggi per verificare e implementare l'SRP.

AGGIORNARE

Classe del rapporto

Ho aggiunto una classe di esempio che viola chiaramente l'SRP. Sarebbe bello se le persone potessero usarlo come esempio per spiegare come si avvicinano al principio della singola responsabilità.

L'esempio è da qui .


Questa è una regola interessante, ma puoi ancora scrivere: "Una classe di persone può renderlo". Ciò può essere considerato una violazione per SRP, poiché l'inclusione della GUI nella stessa classe che contiene regole di business e persistenza dei dati non è OK. Quindi penso che sia necessario aggiungere il concetto di domini architettonici (livelli e livelli) e assicurarsi che questa affermazione sia valida solo con 1 di quei domini (come GUI, Accesso ai dati, ecc.)
NoChance

@EmmadKareem Questa regola è stata menzionata in Head First Object-Oriented Analysis and Design ed è esattamente quello che ho pensato al riguardo. In qualche modo manca un modo pratico per implementarlo. Hanno affermato che a volte le responsabilità non sono così evidenti per il progettista e che deve usare molto buon senso per giudicare se il metodo dovrebbe davvero rientrare in questa classe o no.
Songo,

Se vuoi davvero capire SRP, leggi alcuni degli scritti di zio Bob Martin. Il suo codice è tra i più belli che abbia mai visto, e confido che qualunque cosa dica su SRP non sia solo un buon consiglio, ma è anche più di un semplice cenno della mano.
Robert Harvey,

E il voto negativo potrebbe spiegare perché migliorare il post ?!
Songo,

Risposte:


7

L'SRP afferma, senza mezzi termini, che una classe dovrebbe avere sempre e solo una ragione per cambiare.

Decostruire la classe "report" nella domanda, ha tre metodi:

  • printReport
  • getReportData
  • formatReport

Ignorando il ridondante Reportutilizzato in ogni metodo, è facile capire perché questo viola l'SRP:

  • Il termine "stampa" implica una sorta di interfaccia utente o una vera stampante. Questa classe contiene quindi una parte dell'interfaccia utente o della logica di presentazione. Una modifica ai requisiti dell'interfaccia utente richiederà una modifica alla Reportclasse.

  • Il termine "dati" implica una struttura di dati di qualche tipo, ma non specifica cosa (XML? JSON? CSV?). Indipendentemente da ciò, se i "contenuti" del rapporto cambieranno mai, allora anche questo metodo. È presente un accoppiamento a un database o un dominio.

  • formatReportè solo un nome terribile per un metodo in generale, ma suppongo, osservandolo, che ha ancora una volta a che fare con l'interfaccia utente e probabilmente con un aspetto diverso dell'interfaccia utente rispetto a printReport. Quindi, un'altra ragione non correlata per cambiare.

Quindi questa classe può essere accoppiata con un database, un dispositivo schermo / stampante e una logica di formattazione interna per log o output di file o quant'altro. Avendo tutte e tre le funzioni in una classe, stai moltiplicando il numero di dipendenze e triplicando la probabilità che qualsiasi modifica di dipendenza o requisito interrompa questa classe (o qualcos'altro che dipende da essa).

Parte del problema qui è che hai scelto un esempio particolarmente spinoso. Probabilmente non dovresti avere una classe chiamata Report, anche se fa solo una cosa , perché ... quale rapporto? Non sono tutti "segnalati" animali completamente diversi, basati su dati diversi e requisiti diversi? E un rapporto non è già stato formattato, sia per lo schermo che per la stampa?

Ma guardando oltre, e inventando un ipotetico nome concreto - chiamiamolo IncomeStatement(un rapporto molto comune) - un'architettura "SRPed" corretta avrebbe tre tipi:

  • IncomeStatement- il dominio e / o la classe del modello che contiene e / o calcola le informazioni che compaiono sui rapporti formattati.

  • IncomeStatementPrinter, che probabilmente implementerebbe un'interfaccia standard come IPrintable<T>. Ha un metodo chiave Print(IncomeStatement)e forse alcuni altri metodi o proprietà per la configurazione delle impostazioni specifiche della stampa.

  • IncomeStatementRenderer, che gestisce il rendering dello schermo ed è molto simile alla classe della stampante.

  • Alla fine potresti anche aggiungere altre classi specifiche per funzionalità come IncomeStatementExporter/ IExportable<TReport, TFormat>.

Ciò è notevolmente semplificato nei linguaggi moderni con l'introduzione di generici e contenitori IoC. La maggior parte del codice dell'applicazione non deve fare affidamento sulla IncomeStatementPrinterclasse specifica , può utilizzare IPrintable<T>e quindi operare su qualsiasi tipo di report stampabile, che offre tutti i vantaggi percepiti di una Reportclasse base con un printmetodo e nessuna delle solite violazioni di SRP . L'implementazione effettiva deve essere dichiarata una sola volta, nella registrazione del contenitore IoC.

Alcune persone, di fronte al design di cui sopra, rispondono con qualcosa del tipo: "ma questo sembra un codice procedurale, e il punto centrale di OOP era quello di farci allontanare dalla separazione di dati e comportamento!" A cui dico: sbagliato .

L' IncomeStatementè non solo "dati", e il già citato errore è ciò che provoca un sacco di gente OOP a sentire che stanno facendo qualcosa di sbagliato con la creazione di una tale classe "trasparente" e successivamente iniziare a incepparsi tutti i tipi di funzionalità non correlate alla IncomeStatement(beh, questo e pigrizia generale). Questa classe può iniziare solo come dati ma, nel tempo, garantita, finirà per diventare più un modello .

Ad esempio, un reddito reale affermazione ha un fatturato complessivo , le spese totali e reddito netto linee. Molto probabilmente un sistema finanziario progettato correttamente non li memorizzerà perché non si tratta di dati transazionali - in realtà, cambiano in base all'aggiunta di nuovi dati transazionali. Tuttavia, il calcolo di queste righe sarà sempre esattamente lo stesso, indipendentemente dal fatto che si stia stampando, eseguendo il rendering o esportando il report. Così la vostra IncomeStatementclasse sta per avere una discreta quantità di comportamento ad esso in forma di getTotalRevenues(), getTotalExpenses()e getNetIncome()metodi, e probabilmente molti altri. È un vero oggetto in stile OOP con un suo comportamento, anche se non sembra "fare" molto.

Ma l' formate printmetodi, non hanno nulla a che fare con l'informazione stessa. In effetti, non è troppo improbabile che tu voglia avere diverse implementazioni di questi metodi, ad esempio una dichiarazione dettagliata per la gestione e una dichiarazione non così dettagliata per gli azionisti. La separazione di queste funzioni indipendenti in diverse classi ti dà la possibilità di scegliere diverse implementazioni in fase di esecuzione senza l'onere di un print(bool includeDetails, bool includeSubtotals, bool includeTotals, int columnWidth, CompanyLetterhead letterhead, ...)metodo adatto a tutte le dimensioni . Che schifo!

Spero che possiate vedere dove il metodo sopra, ampiamente parametrizzato, va storto e dove le implementazioni separate vanno bene; nel caso di un singolo oggetto, ogni volta che aggiungi una nuova ruga alla logica di stampa, devi cambiare il tuo modello di dominio ( Tim in finanza vuole i numeri di pagina, ma solo sul rapporto interno, puoi aggiungerlo? ) al contrario di è sufficiente aggiungere una proprietà di configurazione a una o due classi satellitari.

L'implementazione corretta dell'SRP riguarda la gestione delle dipendenze . In poche parole, se una classe fa già qualcosa di utile e stai pensando di aggiungere un altro metodo che introdurrebbe una nuova dipendenza (come un'interfaccia utente, una stampante, una rete, un file, qualunque cosa), non farlo . Pensa invece a come potresti aggiungere questa funzionalità in una nuova classe e come puoi adattare questa nuova classe alla tua architettura complessiva (è abbastanza facile quando si progetta intorno all'iniezione di dipendenza). Questo è il principio / processo generale.


Nota a margine: come Robert, respingo palesemente l'idea che una classe conforme a SRP dovrebbe avere solo una o due variabili di stato. Raramente ci si può aspettare che un involucro così sottile faccia qualcosa di veramente utile. Quindi non esagerare con questo.


+1 davvero un'ottima risposta. Tuttavia, sono solo confuso riguardo alla classe IncomeStatement. Il vostro progetto proposto significherebbe che l' IncomeStatementavrà istanze di IncomeStatementPrinter& IncomeStatementRendererin modo che quando chiamo print()il IncomeStatementche delegherà la chiamata a IncomeStatementPrinterposto?
Songo,

@Songo: Assolutamente no! Non dovresti avere dipendenze cicliche se segui SOLID. Apparentemente la mia risposta non ha chiarito abbastanza che la IncomeStatementclasse non ha un printmetodo, un formatmetodo o qualsiasi altro metodo che non si occupa direttamente di ispezionare o manipolare i dati del report stesso. Ecco a cosa servono quelle altre classi. Se si desidera stamparne uno, si assume una dipendenza IPrintable<IncomeStatement>dall'interfaccia registrata nel contenitore.
Aaronaught l'

aah capisco il tuo punto. Tuttavia, dov'è la dipendenza ciclica se inietto Printerun'istanza nella IncomeStatementclasse? il modo in cui immagino sia quando lo chiamo IncomeStatement.print()lo delegherà a IncomeStatementPrinter.print(this, format). Cosa c'è di sbagliato in questo approccio? ... Un'altra domanda, hai detto che IncomeStatementdovrebbe contenere le informazioni che appaiono sui rapporti formattati se voglio che vengano lette dal database o da un file XML, dovrei estrarre il metodo che carica i dati in una classe separata e delegare la chiamata ad esso in IncomeStatement?
Songo,

@Songo: hai a IncomeStatementPrinterseconda IncomeStatemente a IncomeStatementseconda IncomeStatementPrinter. Questa è una dipendenza ciclica. Ed è solo un cattivo design; non c'è alcun motivo per IncomeStatementsapere qualcosa su un Printero IncomeStatementPrinter- è un modello di dominio, non si occupa della stampa e la delega è inutile poiché qualsiasi altra classe può creare o acquisire un IncomeStatementPrinter. Non ci sono buoni motivi per avere un'idea della stampa nel modello di dominio.
Aaronaught il

Per quanto riguarda il modo in cui si carica IncomeStatementdal database (o file XML) - in genere, è gestito da un repository e / o mapper, non dal dominio e, ancora una volta, non si delega a questo nel dominio; se qualche altra classe ha bisogno di leggere uno di questi modelli, allora richiede esplicitamente quel repository . A meno che tu non stia implementando il modello Active Record immagino, ma non sono davvero un fan.
Aaronaught il

2

Il modo in cui controllo l'SRP consiste nel verificare ogni metodo (responsabilità) di una classe e porre la seguente domanda:

"Dovrò mai cambiare il modo in cui implemento questa funzione?"

Se trovo una funzione che dovrò implementare in diversi modi (a seconda di un tipo di configurazione o condizione), so per certo che ho bisogno di una classe in più per gestire questa responsabilità.


1

Ecco una citazione dalla regola 8 di Calisthenics oggetto :

La maggior parte delle classi dovrebbe semplicemente essere responsabile della gestione di una singola variabile di stato, ma ce ne sono alcune che ne richiederanno due. L'aggiunta di una nuova variabile di istanza a una classe riduce immediatamente la coesione di quella classe. In generale, durante la programmazione in base a queste regole, scoprirai che esistono due tipi di classi, quelle che mantengono lo stato di una singola variabile di istanza e quelle che coordinano due variabili separate. In generale, non mescolare i due tipi di responsabilità

Data questa visione (alquanto idealistica), si potrebbe dire che è improbabile che qualsiasi classe che contenga solo una o due variabili di stato violi SRP. Si potrebbe anche dire che qualsiasi classe che contiene più di due variabili di stato può violare SRP.


2
Questa visione è irrimediabilmente semplicistica. Anche la famosa, ma semplice equazione di Einstein richiede due variabili.
Robert Harvey,

La domanda dei PO era "Esistono altri modi per verificare l'SRP?" - questo è un possibile indicatore. Sì, è semplicistico e non regge in ogni caso, ma è un modo possibile per verificare che SRP sia stato violato.
MattDavey,

1
Ho il sospetto che lo stato mutevole vs immutabile sia anche una considerazione importante
jk.

La Regola 8 descrive il processo perfetto per creare progetti che hanno migliaia e migliaia di classi che rendono il sistema irrimediabilmente complesso, incomprensibile e non realizzabile. Ma il lato positivo è che puoi seguire SRP.
Dunk

@Dunk Non sono in disaccordo con te, ma quella discussione è completamente fuori tema per la domanda.
MattDavey,

1

Una possibile implementazione (in Java). Mi sono preso le libertà con i tipi di ritorno, ma soprattutto penso che risponda alla domanda. TBH Non penso che l'interfaccia per la classe Report sia così male, anche se un nome migliore potrebbe essere in ordine. Ho lasciato fuori dichiarazioni di guardia e affermazioni per brevità.

EDIT: nota anche che la classe è immutabile. Quindi una volta creato non puoi cambiare nulla. È possibile aggiungere un setFormatter () e un setPrinter () e non creare troppi problemi. La chiave, IMHO, è di non modificare i dati non elaborati dopo l'istanza.

public class Report
{
    private ReportData data;
    private ReportDataDao dao;
    private ReportFormatter formatter;
    private ReportPrinter printer;


    /*
     *  Parameterized constructor for depndency injection, 
     *  there are better ways but this is explicit.
     */
    public Report(ReportDataDao dao, 
        ReportFormatter formatter, ReportPrinter printer)
    {
        super();
        this.dao = dao;
        this.formatter = formatter;
        this.printer = printer;
    }

    /*
     * Delegates to the injected printer.
     */
    public void printReport()
    {
        printer.print(formatReport());
    }


    /*
     * Lazy loading of data, delegates to the dao 
     * for the meat of the call.
     */
    public ReportData getReportData()
    {
        if (reportData == null)
        {
            reportData = dao.loadData();
        }
        return reportData;
    }

    /*
     * Delegate to the formatter for formatting 
     * (notice a pattern here).
     */
    public ReportData formatReport()
    {
        formatter.format(getReportData());
    }
}

Grazie per l'implementazione. Ho 2 cose, nella linea if (reportData == null)presumo che tu intenda datainvece. In secondo luogo, speravo di sapere come sei arrivato a questa implementazione. Ad esempio, perché hai deciso di delegare tutte le chiamate ad altri oggetti. Un'altra cosa che mi sono sempre chiesto, è davvero la responsabilità di un rapporto di stamparsi ?! Perché non hai creato una printerclasse separata che prende un reportnel suo costruttore?
Songo,

Sì, reportData = data, mi dispiace per quello. La delega consente un controllo approfondito delle dipendenze. In fase di esecuzione è possibile fornire implementazioni alternative per ciascun componente. Ora puoi avere una HtmlPrinter, PdfPrinter, JsonPrinter, ecc. Questo è utile anche per i test poiché puoi testare i componenti delegati in isolamento e integrato nell'oggetto sopra. Potresti sicuramente invertire la relazione tra stampante e report, volevo solo dimostrare che era possibile fornire una soluzione con l'interfaccia di classe fornita. È abitudine lavorare su sistemi legacy. :)
Heath Lilley,

hmmmm ... Quindi se costruissi il sistema da zero, quale opzione sceglieresti? Una Printerclasse che accetta un rapporto o una Reportclasse che accetta una stampante? In precedenza ho riscontrato un problema simile in cui dovevo analizzare un report e ho discusso con il mio TL se dovremmo creare un parser che accetta un report o se il report deve contenere un parser al suo interno e la parse()chiamata è delegata ad esso.
Songo,

Farei entrambi ... printer.print (report) per avviare e report.print () se necessario in seguito. La cosa grandiosa dell'approccio printer.print (report) è che è altamente riutilizzabile. Separa la responsabilità e ti consente di avere metodi di praticità dove ne hai bisogno. Forse non vuoi che altri oggetti nel tuo sistema debbano conoscere ReportPrinter, quindi avendo un metodo print () su una classe stai raggiungendo un livello di astensione che isola la tua logica di stampa del rapporto dal mondo esterno. Questo ha ancora un ristretto vettore di cambiamento ed è facile da usare.
Heath Lilley,

0

Nel tuo esempio, non è chiaro che SRP sia stato violato. Forse il rapporto dovrebbe essere in grado di formattare e stampare se stesso, se sono relativamente semplici:

class Report {
  void format() {
     text = text.trim();
  }

  void print() {
     new Printer().write(text);
  }
}

I metodi sono così semplici che non ha senso avere ReportFormattero ReportPrinterclassi. L'unico problema evidente nell'interfaccia è getReportDataperché viola i messaggi non dire su oggetti senza valore.

D'altra parte, se i metodi sono molto complicati o ci sono molti modi per formattare o stampare un, Reportallora ha senso delegare la responsabilità (anche più verificabile):

class Report {
  void format(ReportFormatter formatter) {
     text = formatter.format(text);
  }

  void print(ReportPrinter printer) {
     printer.write(text);
  }
}

SRP è un principio di progettazione non un concetto filosofico e quindi si basa sul codice reale con cui stai lavorando. Semanticamente puoi dividere o raggruppare una classe in tutte le responsabilità che desideri. Tuttavia, come principio pratico, SRP dovrebbe aiutarti a trovare il codice che devi modificare . I segni che stai violando SRP sono:

  • Le classi sono così grandi che perdi tempo a scorrere o cercare il metodo giusto.
  • Le lezioni sono così piccole e numerose che perdi tempo a saltare tra di loro o a trovare quella corretta.
  • Quando devi apportare una modifica, influisce su così tante classi che è difficile tenere traccia.
  • Quando è necessario apportare una modifica, non è chiaro quali classi debbano essere modificate.

Puoi risolverli attraverso il refactoring migliorando i nomi, raggruppando insieme un codice simile, eliminando la duplicazione, usando un design a strati e suddividendo / combinando le classi secondo necessità. Il modo migliore per imparare l'SRP è quello di immergersi in una base di codice e ridurre il dolore.


potresti controllare l'esempio che ho allegato al post ed elaborare la tua risposta sulla base di esso.
Songo,

Aggiornato. SRP dipende dal contesto, se hai pubblicato un'intera classe (in una domanda separata) sarebbe più facile da spiegare.
Garrett Hall,

Grazie per l'aggiornamento. Una domanda però, è davvero la responsabilità di un rapporto di stamparsi ?! Perché non hai creato una classe stampante separata che prende un report nel suo costruttore?
Songo,

Sto solo dicendo che SRP dipende dal codice stesso che non dovresti applicarlo dogmaticamente.
Garrett Hall,

sì ho capito il tuo punto. Ma se costruissi il sistema da zero, quale opzione sceglieresti? Una Printerclasse che accetta un rapporto o una Reportclasse che accetta una stampante? Molte volte mi trovo di fronte a una domanda di questo tipo prima di capire se il codice si rivelerà complesso o meno.
Songo,

0

Il principio della responsabilità singola è fortemente associato alla nozione di coesione . Per avere una classe altamente coesa devi avere una dipendenza tra le variabili di istanza della classe e i suoi metodi; cioè, ciascuno dei metodi dovrebbe manipolare quante più variabili di istanza possibile. Più variabili utilizza un metodo, più coerente è per la sua classe; la massima coesione è generalmente irrealizzabile.

Inoltre, per applicare bene SRP si comprende bene il dominio della logica aziendale; per sapere cosa dovrebbe fare ogni astrazione. L'architettura a livelli è anche correlata a SRP, facendo fare in modo che ogni livello faccia una cosa specifica (il livello di origine dati dovrebbe fornire dati e così via).

Tornando alla coesione anche se i tuoi metodi non usano tutte le variabili, dovrebbero essere accoppiati:

public class MyClass {
    private Type1 var1;
    private Type2 var2;
    private Type3 var3;

    public Type3 method1() {
        //use var1 and var3
    }  

    public void method2() {
        //use var1 and var2
    }

    public Type1 method3() {
        //use var2 and var3
    }
}

Non dovresti avere qualcosa di simile al codice qui sotto, in cui una parte delle variabili di istanza viene utilizzata in una parte dei metodi e l'altra parte delle variabili viene utilizzata nell'altra parte dei metodi (qui dovresti avere due classi per ogni parte delle variabili).

public class MyClass {
    private Type1 var1;
    private Type2 var2;
    private Type3 var3;
    private TypeA varA;
    private TypeB varB;

    public Type3 method1() {
        //use var1 and var3
    }  

    public void method2() {
        //use var1 and var2
    }

    public TypeA methodA() {
        //use varA and varB
    }

    public TypeA methodB() {
        //use varA
    }
}
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.