Costruire software SOLID(O)

Una delle sfide più importanti nello sviluppo di applicazioni software critiche per le aziende è la progettazione di valide architetture che siano in grado di assicurare requisiti non funzionali quali l’estensibilità, la manutenibilità, la testabilità e la leggibilità del codice nel tempo.

Quando i clienti ci chiedono infatti di modificare o riscrivere le loro applicazioni, ciò avviene spesso perchè si ritrovano con una base di codice sviluppata male che ha costi e tempi di manutenzione troppo elevati per via delle (mancate) scelte progettuali; in questi casi riscrivere o effettuare il refactoring dell’applicazione con adeguati principi architetturali diventa una scelta da valutare molto seriamente dopo un accurato audit.

Alcuni di questi principii che seguiamo quando lavoriamo sulle applicazioni legacy dei clienti, e non solo, sono sintetizzati nell’acronimo SOLID (Single responsibility, Open-closed, Liskov substitution, Interface segregation, Dependency inversion), un insieme di importanti concetti della programmazione ad oggetti che assieme alle suite di test aiutano a mantenere un’elevata qualità del software.

Applichiamo questi concetti sia quando riscriviamo ex novo l’applicazione che quando procediamo al refactoring. Infatti, sebbene si sia spesso tentati di cestinare la vecchia base di codice per ripartire da una nuova, non sempre questa è la soluzione più economica; in molti casi è infatti sufficiente riutilizzare il codice sufficientemente buono e applicare l’approccio SOLID riscrivendo le parti attorno e quando possibile dentro a quelle porzioni di codice.

Questi sono i principi SOLID:

  • Single Responsibility Principle (SRP): una classe deve farsi carico di una sola responsabilità;
  • Open Closed Principle (OCP): l’estensione dovrebbe essere preferita rispetto alla modifica;
  • Liskov Substitution Principle (LSP): un oggetto di una classe parente dovrebbe essere in grado di fare riferimento a oggetti figli tramite polimorfismo;
  • Interface Segregation Principle (ISP): un client non dovrebbe essere obbligato a utilizzare un’interfaccia se non ne ha bisogno, è meglio avere tante interfacce specifiche piuttosto che una generica per tutti gli scopi;
  • Dependency Inversion Principle (DIP): il codice di più alto livello non deve dipendere dall’implementazione di codice di più basso livello ma dovrebbe dipendere dalle astrazioni (es. tramite iniezione delle dipendenze).

Single Responsibility Principle (SRP)

Il principio della “Single Responsibility” stabilisce che ogni classe deve avere una sola responsabilità e che questa deve essere gestita internamente.

Ad esempio, una classe Prodotto che salva i suoi attributi sul database non dovrebbe definire il proprio codice per aprire la connessione al database ed effettuare il logging di eventuali errori, ma dovrebbe affidarsi ad apposite classi dedicate rispettivamente al database e al logging.

Le classi che violano il principio della singola responsabilità tendono ad essere difficili da sottoporre a unit testing.

Open Closed Principle

Questo principio stabilisce che le classi dovrebbero essere aperte per l’estensione ma chiuse per la modifica.

“Aperte per l’estensione” significa che le classi andrebbero progettate in modo che nuove funzionalità possano essere aggiunte tramite ereditarietà man mano che arrivano nuovi requisiti.

“Chiuse per la modifica” significa che una volta che la classe è stata implementata non dovrebbe essere più modificata, se non per correggere i bug.

Utilizzando interfacce e classi astratte, e strutturando correttamente le dipendenze, nuove funzionalità possono essere aggiunte creando nuove classi che implementano tali interfacce.

Un beneficio collaterale di questo principio è che usare le interfacce per definire le dipendenze comporta la riduzione di accoppiamento e l’ottenimento di codice orientato ai componenti.

Liskov Substitution Principle

Il principio di sostituzione di Liskov si applica all’ereditarietà, specificando che le classi devono essere progettate in modo tale che le dipendenze del client possano essere sostituite con altre sottoclassi senza che il client si accorga della variazione.

In sostanza, quindi, tutte le sottoclassi devono operare nello stesso modo della superclasse. Le funzionalità di una sottoclasse possono essere ovviamente differenti ma devono essere conformi al comportamento che ci si aspetta dalla superclasse.

Interface Segregation Principle

Questo principio stabilisce che le interfacce che sono diventate molto grandi (in modo simile alle “God class”) devono essere suddivise in interfacce più piccole, in modo che il codice client non debba essere forzato a dipendere da metodi dell’interfaccia che non usa e favorendo la composizione rispetto all’ereditarietà.

In questo modo, quando è necessario modificare alcune di queste interfacce non è necessario aggiornare larghe porzioni di codice che in realtà non dovrebbero dipendere da tali modifiche.

Inoltre si ottiene un maggiore disaccoppiamento e il refactoring, il redeploy e la modifica di codice diventano più facili.

Questo principio nasce in Xerox, dove un nuovo sistema di gestione di stampanti con funzionalità aggiuntive come il fax era costruito attorno ad un’unica classe centralizzata invocata per ogni attività.

Questa classe conteneva quindi il codice per inviare un fax anche quando veniva istanziata per effettuare una stampa, rendendo arduo aggiungere e correggere funzionalità, oltre che ad applicare aggiornamenti.

La soluzione applicata prevedeva la suddivisione del codice in più classi specializzate.

Dependency Inversion Principle

Il principio di inversione delle dipendenze stabilisce infine che moduli di alto livello non devono dipendere da moduli di basso livello, devono invece dipendere da astrazioni. Inoltre, le astrazioni non devono dipendere dai dettagli, mentre i dettagli devono dipendere dalle astrazioni.

Questo principio è molto importante per ottenere un forte disaccoppiamento del codice e per renderlo facile da testare in modo isolato, poiché dettagli come il tipo di database non sono “hard coded” ma passati come “plugin”.

Conclusione

I principi SOLID sono strumenti preziosi che dovrebbero essere presi in considerazione sia quando si scrive nuovo codice che quando si effettua il refactoring di sistemi legacy. In particolare dimostrano la loro potenza quando sono usati in combinazione.

Sebbene la sensazione dei programmatori meno esperti che si imbattono in codice e framework progettati con questi principi sia quello di avere a che fare con sistemi “over-engineered”, la realtà è che non usare pienamente questi principi porta a pentirsi di non averlo fatto quando si realizza che il debito tecnico accumulato rende il codice non più mantenibile in modo efficace.

Loading Facebook Comments ...
0 commenti

Lascia un Commento

Vuoi partecipare alla discussione?
Fornisci il tuo contributo!

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *