I Principi SOLID

mattoni

Nel mio primo Post voglio parlarvi di quelli che ritengo essere al giorno d’oggi (ma già da almeno una quindicina di anni),  i paradigmi fondamentali, della programmazione a oggetti. Questi concetti sono indipendenti dal linguaggio utilizzato, dai framework o dalle tecnologie e prendono il nome di principi SOLID.

Solid è un acronimo (su questo blog imparerete che ai Nerd piace inventare acronimi per descrivere i concetti), che sta per:

  • S: single responsibility principle 
  • O: open/close principle
  • L: Liskov Substitution Principle
  • I: interface Segregation Principle
  • D: dependency Inversion

I principi SOLID, sono la base di un qualsiasi programma “scritto bene”. Mi sono trovato molte volte a lavorare su programmi che non rispettavano questi principi e vi posso assicurare che il lavoro si è rivelato molto più complicato e lungo di quanto lo sarebbe stato altrimenti. Attenzione, non parlo dell’inizio di un lavoro,  in quel momento programmare senza seguire uno schema, può sembrare più semplice e veloce, rispetto allo sforzo che dovremmo fare per  seguire degli “schemi concettuali”, ma parlo di quello che è il lavoro quotidiano di un programmatore  mantenere il software. Eh si cari amici, perché se deciderete di fare i programmatori, che voi facciate applicazioni web, o  desktop, o SCADA o App o qualsiasi altra cosa vi venga in mente, la maggior parte del tempo che passerete su un software sarà per correggere dei difetti (bug) o per aggiungere nuove funzionalità (feature).  Vi garantisco,  posso giurarvelo se volete, che se seguirete questi principi il vostro lavoro sarà molto più semplice, altrimenti Benvenuti all’inferno!!! Vi ho convinto?…spero di si…se così è continuate a leggere e scoprite insieme a me come si dovrebbero applicare questi principi:

Single Responsibility Principle

Questo principio in italiano più o meno suonerebbe come “Principio della singola responsabilità”, vale a dire che una classe dovrebbe avere una sola responsabilità, ovvero una classe deve avere un solo motivo per cambiare. 

Mettiamola così, vi piace la pizza? (…vi prego ditemi di si..)…supponiamo di dover scrivere un programma che “fa” la pizza. Questo programma potrebbe sembrare molto semplice da scrivere, facciamo un’unica classe Pizza all’interno della quale scriviamo un metodo faiLaPizza() che si occupa di fare l’impasto, stenderlo, metterci sopra gli ingredienti e cuocere la pizza…diciamo una cosa del genere:

class Pizza
{
    public void faiLaPizza()
    {
        var farina;
        var acqua;
        var lievito;
        var impasto = mescola(farina, lievito, acqua);
        stendi(impasto);
        condisci();
        cuoci();
      }
}

Benissimo! Questa classe è in grado di fare una pizza, ma adesso voglio farvi una domanda, come vi piace la pizza? alta o bassa?…immaginiamo di modificare il metodo faiLaPizz()a, per fare entrambi i tipi di impasto:

class Pizza
{
    public void faiLaPizza()
    {
       var farina;
       var acqua;
       var lievito;
       var impasto = mescola(farina, lievito, acqua);
       if (pizzaAlta)
       { 
          stendiAlto(impasto);
       }
       else
       {
          stendiBasso(impasto)
       }
       condisci();
       cuoci();
   }
}

semplice no? A prima vista forse, ma che cosa succede quando cuociamo i due differenti impasti con lo stesso metodo cuoci? Probabilmente se il metodo cuoci, era stato scritto per la pizza bassa, avremo una pizza alta cruda, viceversa avremo bruciato una pizza bassa; ed è così che abbiamo creato il primo bug nel nostro software per fare la pizza!

Tutto questo succede perché la nostra classe ha troppe responsabilità, deve preoccuparsi di fare l’impasto, stenderlo, condire la pizza e cuocerla, non vi sembrano troppe le  cose che deve fare? quante varianti potremmo avere della pizza? Quanti bug potremmo introdurre? Benvenuti nello spaventoso mondo del software che non rispetta il principio SRP!

Proviamo a riprogettare il software dando ad ogni classe una sola responsabilità. Otterremo una struttura di questo genere:

class Impasto
{
   mescola();
}

class Forma
{
   stendiAlto();
   stendBasso();
}

class Condimento()
{
   condisci();
}

class Cottura
{
  cuociAlto();
  cuociBaso();
}

Adesso ogni classe ha un solo attore, se  vogliamo aggiungere un nuovo metodo di cottura, modificheremo la classe cottura, che si occupa dell’attore “cottura della pizza”, aggiungendo un altro tipo di cottura, ma mantenendo immutate le altre classi. La parte di software da modificare/verificare è notevolmente più piccola e la possibilità di introdurre bug decisamente molto minore.

Abbiamo visto come applicando questo principio il nostro codice sia più facilmente modificabile, mantenibile e controllabile. Questa cari amici è solo la punta dell’iceberg, ma già sembra semplificare notevolmente la nostra vita. Andiamo a scoprire gli altri principi.

Open Close Principle

Il principio “Aperto/Chiuso” asserisce che: “un’entità software dovrebbe essere aperta alle estensioni, ma chiusa alle modifiche”.

Abbiamo applicato questo principio nel nostro programma per fare la pizza?

La risposta ovviamente è no. Vediamo una delle sue possibili applicazioni. Supponiamo di voler fare diversi impasti per la pizza. La classe da dover modificare sarà chiaramente la classe impasto; potremmo fare una cosa del genere:

class Impasto
{
  mescola();
  mescolaIntegrale();
  mescolaFarro();
  mescolaKamut();
}

Niente di grave a prima vista, ma supponete di utilizzare la classe impasto anche per un programma che fa il pane, perché questo programma deve ritrovarsi con la classe Impasto modificata, con tutti i rischi che ne conseguono?  e se in futuro aggiungessimo nuovi tipi di impasto? la classe cambierebbe di nuovo e con essa tutti quelli che la utilizzano.  Riuscite  a percepire il rischio?

Proviamo ad applicare il principio OCP:

interface Impasto
{
    mescola();
}

class ImpastoNormale : Impasto
{
    mescola();
}
class ImpastoIntegrale : Impasto
{
    mescola();
}
class ImpastoFarro : Impasto
{
    mescola();
}
class ImpastoKamut : Impasto
{
    mescola();
}

A questo punto se volessimo aggiungere un nuovo impasto basterà creare una nuova estensione dell’interfaccia impasto senza modificare il software esistente e tutti quelli che lo utilizzano. Adesso la nostra classe è aperta alle estensioni ma chiusa alle modifiche. Se volete potete esercitarvi a trovare altre classi del nostro “programma” che possono essere ripensate secondo questo principio.

Liskov Substitution Principle

La definizione di questo principio può risultare un po’ ostica, ma suona più o meno così: “Se è una proprietà che si può dimostrare essere valida per oggetti x di tipo T, allora deve essere valida per oggetti y di tipo S dove S è un sottotipo di T”.  In pratica questo principio definisce la sostituibilità degli oggetti e cioè in un programma che utilizza oggetti di tipo T, le sue proprietà possono essere sostituite con quelle del suo sottotipo S, senza alterare il funzionamento del programma. Vediamo un esempio di violazione di questo principio sulla nostra classe impasto, riadattata  per i nostri scopi:

class Program
{
    static void stampaNome(Impasto impasto)
    {
        Console.WriteLine(impasto.getMattarello());
    }

}


public class Impasto
{
  public string getMattarello();
  public void setMattarello(string nomeMattarello)
  mescola();
}


class ImpastoIntegrale : Impasto
{
  public string getSpatola();
  public void setSpatola(string nomeSpatola)
  mescola();
}

Supponiamo di avere un programma che stampa il nome del mattarello con cui impastiamo, supponiamo di sostituire la classe impasto con la classe impastoIntegrale; quest’ultima non ha una proprietà che restituisce il nome del mattarello, quindi Il nostro programma restituirebbe un errore!

L’OGGETTO impastoIntegrale VIOLA IL PRINCIPIO DI LISKOV.

Potremmo pensare di risolvere la cosa modificando il nostro programma in questo modo:

class Program
{
  static void stampaNome(Impasto impasto)
  {
    if(impasto.GetType().Equals(typeof(Impasto)))
      Console.WriteLine(impasto.getMattarello());
    else if (impasto.GetType().Equals(typeof(ImpastoIntegrale)))
      Console.WriteLine(impasto.GetSpatola());
  }
}

Tuttavia una modifica del genere vìola il principio open/close perché la classe program diviene aperta a tutte le modifiche della classe Impasto e delle sue classi derivate. A questo problema ci possono essere diverse soluzioni. Si potrebbe eliminare l’ereditarietà (Sconsigliato), perché dovremmo riscrivere tutti i metodi ereditati e la duplicazione è IL Peccato Originale della programmazione!

Un’altra soluzione potrebbe essere quella di inserire nuovi livelli di astrazione, fino ad ottenere delle entità sostituibili (Sconsigliato), si rischia di produrre una grande quantità di classi e livelli e tanto codice in più da modificare e manutenere. La soluzione che prediligo (e che consiglio) è quella di implementare i diversi comportamenti sia nella classe base che nella classe figlia e fare in modo, che il nostro programma si limiti ad invocare i metodi senza preoccuparsi delle implementazioni. Questo è un principio abbastanza “ostico”, ma rispettare il principio di Liskov ci aiuta anche a rispettare il principio open/close ad esso strettamente correlato e a mantenere flessibile il nostro software.

Dependency Inversion Principle

Il principio di inversione delle dipendenze è stato formulato da Robert C. Martin (Uncle Bob), un vero e  proprio guru della progettazione software, del quale vi consiglio di leggere alcuni libri come Clean Code e Clean Architecture .

L’enunciato è questo:

I moduli di alto livello non devono dipendere da quelli di basso livello. Entrambi devono dipendere da astrazioni ; Le astrazioni non devono dipendere dai dettagli; sono i dettagli che dipendono dalle astrazioni.

Nella programmazione classica siamo abituati a pensare di sviluppare i moduli o le funzionalità  di basso livello, ad esempio le operazioni sul database o sul file system e poi di far utilizzare ai moduli di alto livello, quelli di basso livello. Questo però crea una forte dipendenza dei programmi dalle librerie di servizio, costringendo la logica operativa dei nostri software a sottostare alle regole di queste librerie.

Facciamo l’esempio di un oggetto documento che deve essere salvato su Database. Nella programmazione classica, il nostro programma suonerebbe più o meno così:

Class Documento
{
   void salvaDocumento()
  {
     Database db = new DataBase();
     db.insert("DocTable","param1","param2",...); 
  }
}

In questo modo però la mia classe documento dipende in tutto e per tutto, dal  metodo salva della classe database. E’ evidente che una modifica a tale metodo, mi costringerebbe a modificare  tutte le classi che usano la libreria per salvare sul database. Vediamo cosa cambia applicando il principio di inversione delle dipendenze. Quello che dobbiamo fare è astrarre un modello di documento ed astrarre un modello di database in questo modo:

class Modello
{ 
   DataBase db = new DataBase();
   void salva()
   {
     db.salva()
   }
}
class Documento : Modello
{
        salva()
}

class Database{
  void salva()
  void aggiorna()
  ....
}
class MySqlDb : Database{
  void salva()
  {
  }

  void aggiorna()
  {
  }
}
Lo vedete il  vantaggio che abbiamo ottenuto?  Adesso il nostro sistema è estremamente più flessibile! Possiamo aggiungere nuove classi, che estendendo Modello, già beneficiano dei metodi di database e viceversa possiamo scrivere nuovi tipi di accesso al database, senza dover modificare le classi che già usano i database esistenti. Volendo potremmo decidere di distribuire la nuova libreria di accesso al database in altri progetti ed eventualmente estendere il modello di database, con un nuovo accesso specifico per la nuova applicazione.
Con questo si conclude il nostro viaggio tra i principi solid. Spero di avervi trasmesso quelli che sono i concetti fondamentali ed il motivo per cui  è indispensabile utilizzare questi paradigmi, quando si progetta un software…
Se avete domande su questo argomento o richieste di argomenti che vi piacerebbe che fossero trattati in questo blog, non avete che da chiedere.
Continuate a seguirmi su “Riflessioni di un Nerd”…Al prossimo viaggio nel mirabolante mondo del software!

 

Advertisements

Published by

Gabriele Predellini

Che dire su di me...sono un ingegnere informatico, scrivo (o almeno ci provo) programmi software da un decennio. Il software mi appassiona e mi incuriosisce, cerco sempre di migliorarmi e di stare al passo con le ultime tecnologie, non perdendo mai di vista un concetto fondamentale...c'è sempre qualcosa di nuovo da imparare!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s