SOLIDO Parte 1 - Il principio della singola responsabilità

Single Responsibility (SRP), Open / Close, Liskov's Substitution, Interface Segregation e Dependency Inversion. Cinque principi agili che dovrebbero guidarti ogni volta che scrivi il codice.

La definizione

Una classe dovrebbe avere solo una ragione per cambiare.

Definito da Robert C. Martin nel suo libro Agile Software Development, Principles, Patterns and Practices e successivamente ripubblicato nella versione C # del libro Agile Principles, Patterns and Practices in C #, è uno dei cinque principi dell'agilità SOLID. Ciò che afferma è molto semplice, tuttavia il raggiungimento di tale semplicità può essere molto difficile. Una classe dovrebbe avere solo una ragione per cambiare.

Ma perché? Perché è così importante avere una sola ragione per il cambiamento?

In linguaggi tipizzati staticamente e compilati, diversi motivi possono portare a diverse ridistribuzioni indesiderate. Se ci sono due diversi motivi per cambiare, è ipotizzabile che due squadre diverse possano lavorare sullo stesso codice per due diversi motivi. Ognuno dovrà implementare la propria soluzione, che nel caso di un linguaggio compilato (come C ++, C # o Java), può portare a moduli incompatibili con altri team o altre parti dell'applicazione.

Anche se non è possibile utilizzare una lingua compilata, potrebbe essere necessario ripetere il test della stessa classe o modulo per ragioni diverse. Ciò significa più lavoro di qualità, tempo e impegno.

Il pubblico

Determinare l'unica responsabilità che una classe o un modulo dovrebbe avere è molto più complessa di una semplice lista di controllo. Ad esempio, un indizio per trovare le nostre ragioni per il cambiamento è quello di analizzare il pubblico per la nostra classe. Gli utenti dell'applicazione o del sistema che sviluppiamo che sono serviti da un particolare modulo saranno quelli che ne chiederanno le modifiche. Quelli serviti chiederanno il resto. Ecco un paio di moduli e il loro possibile pubblico.

  • Modulo di persistenza - Il pubblico include DBA e architetti del software.
  • Modulo di segnalazione - Il pubblico include impiegati, contabili e operazioni.
  • Modulo di calcolo del pagamento per un sistema di buste paga - Il pubblico può includere avvocati, manager e contabili.
  • Modulo di ricerca libri per un sistema di gestione della libreria - Il pubblico può includere il bibliotecario e / oi clienti stessi.

Ruoli e attori

Associare persone concrete a tutti questi ruoli può essere difficile. In una piccola azienda una singola persona potrebbe aver bisogno di soddisfare diversi ruoli, mentre in una grande azienda potrebbero esserci più persone assegnate a un singolo ruolo. Quindi sembra molto più ragionevole pensare ai ruoli. Ma i ruoli da soli sono abbastanza difficili da definire. Che ruolo ha? Come lo troviamo? È molto più facile immaginare attori che svolgono quei ruoli e associano il nostro pubblico a quegli attori.

Quindi se il nostro pubblico definisce le ragioni del cambiamento, gli attori definiscono il pubblico. Questo ci aiuta molto a ridurre il concetto di persone concrete come "John the architect" in Architecture, o "Mary the referent" in Operations.

Quindi una responsabilità è una famiglia di funzioni che serve un particolare attore. (Robert C. Martin)

Fonte di cambiamento

Nel senso di questo ragionamento, gli attori diventano una fonte di cambiamento per la famiglia di funzioni che li serve. Quando i loro bisogni cambiano, anche quella specifica famiglia di funzioni deve cambiare per adattarsi ai loro bisogni.

Un attore per una responsabilità è l'unica fonte di cambiamento per quella responsabilità. (Robert C. Martin)

Esempi classici

Oggetti che possono "stampare" se stessi

Diciamo che abbiamo un Libro classe che racchiude il concetto di un libro e le sue funzionalità.

class Book function getTitle () return "A Great Book";  function getAuthor () return "John Doe";  function turnPage () // puntatore alla pagina successiva function printCurrentPage () echo "current page content"; 

Questo può sembrare una classe ragionevole. Abbiamo un libro, può fornire il titolo, l'autore e può girare la pagina. Infine, è anche in grado di stampare la pagina corrente sullo schermo. Ma c'è un piccolo problema. Se pensiamo agli attori coinvolti nella gestione del Libro oggetto, chi potrebbero essere? Possiamo facilmente pensare a due attori diversi: Gestione del libro (come il bibliotecario) e Meccanismo di presentazione dei dati (come il modo in cui vogliamo fornire il contenuto all'utente: interfaccia grafica, interfaccia grafica, interfaccia utente solo testo, stampa) . Questi sono due attori molto diversi.

Mescolare la logica aziendale con la presentazione è negativo, perché è contro il Principio di Responsabilità Unica (SRP). Dai un'occhiata al seguente codice:

class Book function getTitle () return "A Great Book";  function getAuthor () return "John Doe";  function turnPage () // puntatore alla pagina successiva function getCurrentPage () return "current page content";  interfaccia Printer function printPage ($ page);  classe PlainTextPrinter implementa Printer function printPage ($ page) echo $ page;  classe HtmlPrinter implementa Printer function printPage ($ page) echo '
'. $ pagina. '
';

Anche questo esempio di base mostra come separare la presentazione dalla logica aziendale e rispettare l'SRP, offre grandi vantaggi nella flessibilità del nostro design.

Oggetti che possono "salvarsi"

Un esempio simile a quello sopra è quando un oggetto può salvare e recuperare se stesso dalla presentazione.

class Book function getTitle () return "A Great Book";  function getAuthor () return "John Doe";  function turnPage () // puntatore alla pagina successiva function getCurrentPage () return "current page content";  function save () $ filename = '/ documents /'. $ This-> getTitle (). '-'. $ This-> getAuthor (); file_put_contents ($ filename, serialize ($ this)); 

Possiamo, ancora una volta identificare diversi attori come Book Management System e Persistenza. Ogni volta che vogliamo cambiare la persistenza, dobbiamo cambiare questa classe. Ogni volta che vogliamo cambiare il modo in cui passiamo da una pagina alla successiva, dobbiamo modificare questa classe. Ci sono diversi assi di cambiamento qui.

class Book function getTitle () return "A Great Book";  function getAuthor () return "John Doe";  function turnPage () // puntatore alla pagina successiva function getCurrentPage () return "current page content";  classe SimpleFilePersistence function save (Book $ book) $ filename = '/ documents /'. $ book-> getTitle (). '-'. $ Libro-> getAuthor (); file_put_contents ($ filename, serialize ($ book)); 

Spostare l'operazione di persistenza in un'altra classe separerà chiaramente le responsabilità e saremo liberi di scambiare i metodi di persistenza senza influire sul nostro Libro classe. Ad esempio implementando a DatabasePersistence la lezione sarebbe banale e la nostra logica di business costruita attorno alle operazioni con i libri non cambierebbe.

Una vista a più alto livello

Nei miei precedenti articoli ho spesso menzionato e presentato lo schema architettonico di alto livello che può essere visto sotto.


Se analizziamo questo schema, puoi vedere come viene rispettato il Principio di Responsabilità Unica. La creazione dell'oggetto è separata a destra in Fabbriche e il punto di ingresso principale della nostra applicazione, una responsabilità di attore uno. Anche la persistenza è gestita in fondo. Un modulo separato per la responsabilità separata. Infine, a sinistra, abbiamo la presentazione o il meccanismo di consegna, se lo desideri, sotto forma di MVC o qualsiasi altro tipo di interfaccia utente. SRP ha di nuovo rispettato. Non resta che capire cosa fare all'interno della nostra logica aziendale.

Considerazioni sulla progettazione del software

Quando pensiamo al software che dobbiamo scrivere, possiamo analizzare molti aspetti diversi. Ad esempio, diversi requisiti che riguardano la stessa classe possono rappresentare un asse di cambiamento. Questo asse del cambiamento può essere un indizio per una singola responsabilità. C'è un'alta probabilità che i gruppi di requisiti che interessano lo stesso gruppo di funzioni avranno motivi per cambiare o essere specificati in primo luogo.

Il valore primario del software è la facilità di cambiamento. Il secondario è la funzionalità, nel senso di soddisfare il maggior numero possibile di requisiti, soddisfacendo le esigenze dell'utente. Tuttavia, per ottenere un alto valore secondario, è obbligatorio un valore primario. Per mantenere alto il nostro valore primario, dobbiamo avere un design che sia facile da modificare, ampliare, accogliere nuove funzionalità e assicurare che SRP sia rispettato.

Possiamo ragionare in modo graduale:

  1. Un alto valore primario porta in tempo ad un alto valore secondario.
  2. Valore secondario significa necessità degli utenti.
  3. Le esigenze degli utenti significano esigenze degli attori.
  4. Le esigenze degli attori determinano i bisogni dei cambiamenti di questi attori.
  5. Le esigenze di cambiamento degli attori definiscono le nostre responsabilità.

Quindi quando progettiamo il nostro software dovremmo:

  1. Trova e definisci gli attori.
  2. Identificare le responsabilità che servono a quegli attori.
  3. Raggruppa le nostre funzioni e le nostre classi in modo che ognuna abbia una sola responsabilità assegnata.

Un esempio meno ovvio

class Book function getTitle () return "A Great Book";  function getAuthor () return "John Doe";  function turnPage () // puntatore alla pagina successiva function getCurrentPage () return "current page content";  function getLocation () // restituisce la posizione nella libreria // ie. numero di scaffale e numero della camera

Ora questo può sembrare perfettamente ragionevole. Non abbiamo metodi che riguardano la persistenza o la presentazione. Abbiamo il nostro turnPage () funzionalità e alcuni metodi per fornire diverse informazioni sul libro. Tuttavia, potremmo avere un problema. Per scoprirlo, potremmo voler analizzare la nostra applicazione. La funzione getLocation () potrebbe essere il problema.

Tutti i metodi del Libro la classe riguarda la logica del business. Quindi la nostra prospettiva deve essere dal punto di vista del business. Se la nostra applicazione è scritta per essere utilizzata da veri bibliotecari che cercano libri e ci danno un libro fisico, allora l'SRP potrebbe essere violato.

Possiamo pensare che le operazioni degli attori siano quelle interessate ai metodi getTitle (), getAuthor () e getLocation (). I clienti possono anche avere accesso all'applicazione per selezionare un libro e leggere le prime pagine per avere un'idea del libro e decidere se lo vogliono o no. Quindi i lettori degli attori potrebbero essere interessati a tutti i metodi tranne getLocations (). Un cliente ordinario non si cura di dove il libro è tenuto in biblioteca. Il libro sarà consegnato al cliente dal bibliotecario. Quindi, abbiamo effettivamente una violazione di SRP.

class Book function getTitle () return "A Great Book";  function getAuthor () return "John Doe";  function turnPage () // puntatore alla pagina successiva function getCurrentPage () return "current page content";  class BookLocator function locate (Book $ book) // restituisce la posizione nella libreria // ie. numero di scaffale e numero di camera $ libraryMap-> findBookBy ($ book-> getTitle (), $ book-> getAuthor ()); 

Presentazione del BookLocator, il bibliotecario sarà interessato al BookLocator. Il cliente sarà interessato al Libro solo. Naturalmente, ci sono diversi modi per implementare un BookLocator. Può usare l'autore e il titolo o un oggetto del libro e ottenere le informazioni richieste dal Libro. Dipende sempre dalla nostra attività. Ciò che è importante è che se la libreria viene cambiata e il bibliotecario dovrà trovare libri in una libreria diversamente organizzata, il Libro l'oggetto non sarà interessato Allo stesso modo, se decidiamo di fornire un riassunto pre-compilato ai lettori invece di lasciarli sfogliare le pagine, ciò non influirà sul bibliotecario né sul processo di trovare lo scaffale su cui i libri si trovano.

Tuttavia, se la nostra attività consiste nell'eliminare il bibliotecario e creare un meccanismo di self-service nella nostra biblioteca, allora possiamo considerare che SRP è rispettato nel nostro primo esempio. I lettori sono anche i nostri bibliotecari, devono andare a cercare il libro da soli e poi controllarlo al sistema automatico. Questa è anche una possibilità. Quello che è importante ricordare qui è che devi sempre considerare attentamente la tua attività.

Pensieri finali

Il principio della singola responsabilità dovrebbe essere sempre considerato quando scriviamo il codice. Il design delle classi e dei moduli ne è fortemente influenzato e porta a un design a basso accoppiamento con dipendenze meno e più leggere. Ma come ogni moneta, ha due facce. Si sta tentando di progettare sin dall'inizio la nostra applicazione con SRP in mente. È anche allettante identificare tutti gli attori che vogliamo o di cui abbiamo bisogno. Ma questo in realtà è pericoloso - dal punto di vista del design - per cercare di pensare a tutte le parti fin dall'inizio. L'eccessiva considerazione dell'SRP può facilmente portare a un'ottimizzazione prematura e invece di un design migliore, può portare a una dispersione in cui le chiare responsabilità delle classi o dei moduli possono essere difficili da capire.

Quindi, ogni volta che osservi che una classe o un modulo inizia a cambiare per motivi diversi, non esitare, adotta i provvedimenti necessari per rispettare SRP, tuttavia non devi attendere perché l'ottimizzazione prematura può facilmente ingannarti.