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 Report
utilizzato 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 Report
classe.
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 IncomeStatementPrinter
classe specifica , può utilizzare IPrintable<T>
e quindi operare su qualsiasi tipo di report stampabile, che offre tutti i vantaggi percepiti di una Report
classe base con un print
metodo 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 IncomeStatement
classe 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' format
e print
metodi, 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.