SRP, Single Responsibility Principle

_Logo La definizione originale data da Uncle Bob indica che “una classe deve avere uno e un solo motivo per cambiare”. In pratica si assegna alla classe una sola responsabilità e la classe deve essere modificata solo se tale responsabilità cambia.

Il concetto è stato esteso ad ogni elemento di un programma (progetto, classe e metodo): ogni elemento deve avere una sola responsabilità e tale responsabilità deve essere interamente incapsulata dall'elemento stesso.

Tale principio ha come base due concetti chiave della progettazione software: la coesione (cohesion) e il legame/accoppiamento (coupling):

  • Il livello di coesione di un elemento permette di valutare se le parti che lo compongono sono strettamente collegati alla sua singola responsabilità. Ad esempio i metodi di una classe devono essere collegati alla responsabilità della classe e non fare qualcosa in più solo perché possono ricevere dei parametri.
  • Il livello di accoppiamento permette invece di valutare il grado di dipendenza dell'elemento rispetto ad altri elementi. Quindi se una classe ha tante responsabilità tende ad utilizzare una vasta gamma di altre classi.

La regola fondamentale low coupling, high cohesion permette di avere classi con una singola responsabilità e il più indipendenti possibili rispetto alle altre classi. I concetti sono legati tra loro perché in generale un'alta coesione porta ad avere legami stretti.

La definizione di Uncle Bob pone l'accento sui motivi che spingono a modificare una classe. Il motivo è legato in genarle alla modifica dei requisiti dell'applicativo, ai casi d'uso dell'utente.

Tali modifiche cambiano le responsabilità di uno o più elementi. Ne consegue che:

  • Se un elemento ha più di una responsabile è più probabile che venga modificato: ciò può portare alla modifica (anche accidentale) delle altre responsabilità.
  • Se una singola responsabilità è suddivisa su più elementi significa che la modifica comporti il dover intervenire su più parti del software.
  • Più elementi del software vengono coinvolte in una modifica più alta è la possibilità di introdurre errori, in particolare si crea una catena di conseguenze più gande di quanti ci si aspetti.
  • Avere elementi con singole responsabilità significa poter intervenire in modo mirato, sia per correggere un errore sia per modificare o estendere delle funzionalità.
  • Avere una sola responsabilità per elemento significa identificare subito cosa dover modificare: quel progetto, quella classe, quel metodo. Ciò aiuta la valutazione dei costi legati alla modifica.

Seguire tale principio significa semplificare il design generale dell'applicativo, anche se ciò spesso porta ad avere molti elementi in più: in fase di refactoring non è raro suddividere una classe in più classi, spesso decide collegate a nuove interfacce. Apparentemente la complessità del progetto aumenta, ma tutti questi oggetti possono essere valutati con diversi livelli di astrazione.

Di seguito alcuni tipici esempi di violazione di tale principio:

  • Includere in una classe che rappresenta una entità le operazioni di persistenza. L'entità ha già la responsabilità di mantenere un insieme di dati, la persistenza deve essere spostata in una classe apposita. Avremo così una classe Customer e CustomerDataAcess.
  • Includere in una classe che rappresenta un'entità anche la logica di rappresentazione tramite UI: anche in questo caso bisogna dividere le classi, ottenendo ad esempio Customer e CustomerDiplay.
  • Includere in un classe che gestire un ordine il codice per salvare l’ordine e inviare le notifiche. Sempre per esempio avremo OrderProcess, OrderDataAccess e un servizio per l’invio delle notifiche, magari rappresentato da un generico INotificationService.
  • Mescolare entità (i data) ed elaborazione (o in generale la business logic) è una tipica violazione del principio di singola responsabilità.

Perché creare tante classi?

  • Perché una modifica di una fat class è più probabile che influenzi più aspetti rispetto ad un modifica di una classi più mirata (focused).
  • Perché una classe di piccole dimensioni è più semplice da comprendere e modificare.
  • Perché, considerando la coppia Customer e CustomerDisplay, se voglio presentare le informazioni del cliente in modo diverso, allora posso creare una nuova classe Display alternativa, ad esempio CustomerDetailedDisplay. In questa classe potrei far caricare informazioni più dettagliate senza modifica Customer.
  • Perché includere più responsabilità significa che estendendo la classe con altre richieste si finisce ad avere molti flag di configurazione per attivare una funzionalità piuttosto che un'altra. Si finisce ad avere strutture condizionali complesse difficili da manutenere.

In fase di design è utile mantenere una certa visuale a astratta e identificare dapprima le entità e quindi le responsabilità coinvolte nei processi che l’applicativo deve svolgere. Le entità danno vita alle classi, mentre le seconde possono essere definite tramite interfacce e classi astratta, come nel caso di INotificationService che è facilmente implementabile in modo diverso.

Inoltre tale struttura permette di passare agli oggetti le astrazione di cui hanno bisogno rendendo chiaro quali sono le dipendenze ma senza creare un legame stretto con alcuna specifica implementazione.

Stabilire ruoli distinti e astrarli significa inoltre renderli riusabili evitando duplicazione di codice, tipica conseguenza del mescolare le responsabilità.

Abbiamo quindi seguito un altro importante principio detto interface segregation principle (ISP) che è spesso d'aiuto alla singola responsabilità.

Un altro campo in cui il principio di singola responsabilità può migliorare il codice riguarda la gestione di oggetti dello stesso genere ma di tipi diversi. Nella definizione di un Customer potremmo aggiungere una semplice enumerazione per indicare il tipo (Company, Administration, Citizen). Ma probabilmente ogni tipo richiede delle informazioni diverse dall’altro ed incorporare tutto nella classe Customer significa assegnare a Customer più compiti. Meglio creare classi specializzare: CompanyCustomer, AdministrationCustomer, CitizenCustomer.

Conseguenze:

  • Diminuiscono le branching logic nel codice: non sono necessari blocchi if o switch per stabilire cosa fare.
  • Il numero delle dipendenze tra elementi diminuisce e diventa più chiaro cosa effettivamente un elemento richiede.
  • Evitare che le responsabilità sia accoppiare tra loro, quindi una modifica ad una non deve inibirne un’altra.
  • Diminuire il numero di elementi da modifica in caso di modifica.
  • Aumenta il riuso del codice perché ogni elemento ha uno specifico compito.

Argomenti collegati:

Riferimenti:

  • Object Mentor
  • Wikipedia
  • “Clean Code: A Handbook of Agile Software Craftsmanship” (Robert C. Martin)
  • “Agile Principles, Patterns, and Practices in C#” (Robert C. Martin; Micah Martin)

Tag: SOLID, SRP