Il Principio
La S di S.O.L.I.D. sta per “Single Responsibility Principle”. Per “Principio delle Singole Responsabilità” o meglio “Principio di Isolamento delle Responsabilità”, che in tutta onestà preferisco.
Partiamo dalla sua definizione precisa in inglese, ripresa da Wikipedia.
“Every class should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class.”
… che si può tradurre come:
“Ogni classe dovrebbe avere una ed una sola responsabilità rispetto alle funzionalità fornite dal software, e tale responsabilità dovrebbe essere totalmente ed interamente incapsulata nella classe.”
La Spiegazione
Parafrasando la regola e lasciando da parte i vari paroloni del caso, possiamo tranquillamente leggere tutto questo come:
“Ogni classe deve occuparsi di svolgere un solo compito. La funzionalità relativa a tale compito deve essere interamente implementata all’interno della classe stessa.”
I nostri software presentano varie funzionalità, a volte ce ne sono a decine, centinaia.
Soprattutto quando abbiamo a che fare con un’applicazione complessa, dividere il codice in più classi in modo intelligente è fondamentale per ottenere una qualità del codice superiore alla media.
Il Single Responsibility Principle, appunto, suggerisce che criterio usare per decidere quando dividere una classe in più classi ed isolarne le responsabilità.
Proviamo a vedere, tramite un esempio, l’applicazione del principio.
L’Esempio
Stiamo scrivendo un software per un’azienda che svolge svariate analisi finanziarie. Come capita spesso in queste tipologie di software, ci sono delle funzionalità che permettono di creare dei report, magari inviandoli per posta o salvandoli su un file.
Durante l’analisi preliminare necessaria a capire il lavoro da fare, abbiamo appreso della necessità di poter creare un report mensile dei ricavi, da salvare su file.
Abbiamo quindi realizzato una classe CreateMonthlyReport, dedicata allo scopo. Eccola:
revenuesData = $this->getData($month, $year);
$this->saveOnFile($filePath);
}
// metodo privato che preleva i dati dal database
private function getData()
{
$results = [];
// creo ed eseguo una query SQL che mi restituisca i risultati per
// mese ed anno impostati
return $results;
}
// metodo privato che salva i dati su file
private function saveOnFile($filePath)
{
// salvo i dati ottenuti su file il cui percorso è $filePath
}
}
Prima che tu pensi qualsiasi cosa… sappi che una cosa del genere è sbagliata.
Perché? Ci si arriva molto velocemente.
Diciamo che il software scritto per l’azienda ha soddisfatto il cliente. Siamo alle prime iterazioni di sviluppo ma procede tutto bene e la compagnia è contenta del lavoro.
Un bel giorno gli analisti ti chiamano: è arrivato il momento di aggiungere nuove feature. Sei felicissimo della cosa ma la sensazione svanisce immediatamente, non appena ti chiedono due “personalizzazioni”:
- il software deve funzionare su database Oracle, ed anche molto velocemente;
- il software deve adesso inviare i report via email, e non più salvarli su file;
Nel giro di due secondi diventi bianco e stramazzi a terra, morto di paura.
Ed ecco che arriviamo al punto.
Prova a guardare la classe precedente, e dimmi cosa fa.
Allora, vediamo… prende in input mese ed anno dal costruttore, quindi di conseguenza prende i dati richiesti dal database ed infine li salva sul file.
Ora, torna indietro e rileggi l’enunciato del Single Responsibility Principle.
Visto? La tua classe non fa una cosa sola, ma ben tre!
Bisogna quindi separare le responsabilità, creando più classi e rendendo più “atomiche” le operazioni.
Guarda qui:
$sqlDbQueryEngine = new SQLDatabaseQueryEngine;
$this->fileWriter = new FileWriter;
}
public function save($month, $year, $filePath)
{
$this->revenuesData = $this->$sqlDbQueryEngine->getMonthlyReportData($month, $year);
$this->fileWriter->save($filePath, $this->revenuesData);
}
}
Come puoi vedere, le cose sono cambiate radicalmente.
Separando le responsabilità, ci siamo assicurati di far svolgere ad ogni classe uno ed un solo compito. La classe CreateMonthlyReport effettua la stessa operazione di prima, ma al posto di svolgere tutti i compiti da sola si assicura di usare SQLDatabaseQueryEngine e FileWriter, rispettivamente, per le operazioni di recupero dei dati e scrittura su file.
Abbiamo tre classi al posto di una, certo, ma è anche vero che sono tutte e tre piccole, molto leggibili e di conseguenza facilmente manutenibili.
Passare da SQL ad Oracle a questo punto diventerebbe davvero semplice: basterebbe usare un eventuale nuovo componente OracleDbQueryEngine e non più SQLDatabaseQueryEngine, evitando inoltre di riscrivere un terzo del codice di ogni classe interessata!
Isolare le responsabilità, insomma, ti permette di creare in modo efficace dei veri e propri mattoncini di codice, da usare e riusare nei momenti più opportuni.
Avrai già sentito parlare di D.R.Y. (Don’t Repeat Yourself). Il Single Responsibility Principle è una delle cose più DRY che incontrerai nella tua vita.
Detto questo passiamo avanti, l’Open/Closed Principle ci attende.