Il Principio
Oggi vediamo di conoscere meglio il secondo principio S.O.L.I.D. Precisamente, la “O” che sta per Open/Closed Principle, traducibile come “Principio Aperto/Chiuso”.
Partiamo con un consiglio: per carità divina, usa i nomi in inglese.
Scherzi a parte, ecco l’enunciato preciso del principio.
“Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.”
Tradotto in italica lingua possiamo scrivere:
“Le entità di un software (classi, moduli, funzioni e così via) dovrebbero essere aperte all’estensione e chiuse alla modifica.”
La Spiegazione
Probabilmente non hai bisogno di spiegazione vista la facilità di lettura, stavolta, ma mi permetto comunque di puntualizzare per ovvie ragioni di completezza.
In poche parole, l’Open/Closed Principle dice che partendo da un insieme di classi che hai realizzato per risolvere un problema, l’unico motivo per cui dovresti mai decidere di toccare tali classi dovrebbe essere solo ed esclusivamente la correzione di bug.
Non dovresti mai toccare una classe per andarci ad aggiungere funzionalità: per tale scopo, infatti, dovresti piuttosto estendere la classe in questione con una nuova.
Il concetto di ereditarietà è la base portante di questo principio, il cui nome è stato proposto per la prima volta da Bertrand Meyer nel 1988, nel suo libro Object Oriented Software Construction.
Tra l’altro, c’è da notare un’altra cosa interessante. Se ci pensi, infatti, applicare bene questo principio significa anche applicare, indirettamente, il Single Responsibility Principle. D’altronde, se crei una classe che deve fare solo una cosa e tale classe funziona bene, che altri motivi dovresti avere per cambiare le carte in tavola?
Questo è un aspetto molto interessante dei principi S.O.L.I.D. Non si parla di elementi divisi tra loro, ma di un’insieme di principi fortemente connessi.
Passiamo adesso all’esempio pratico, per renderci conto meglio di cosa stiamo affrontando.
L’Esempio
Inventare nuovamente la ruota non ha molto senso, per cui userò una versione modificata di un esempio che ho letto altrove e che mi ha colpito molto per la sua semplicità.
Immaginiamo di lavorare con un software che si occupa di effettuare calcoli su rettangoli. Il mondo è pieno di rettangoli, quindi servono software che lavorino con queste forme. Ora, a livello di classi ed oggetti un rettangolo può essere rappresentato facilmente così:
width = $width;
$this->height = $height;
}
}
Niente di complesso.
Ora, questo nostro software, tra le varie funzionalità, offre la possibilità di calcolare la somma dei perimetri di un insieme di rettangoli i cui dati sono inseriti dall’utente.
La prima cosa che ci verrà in mente, quindi, sarà creare una classe PerimeterCalculator che si comporti in questo modo:
width * 2 + $rectangle->height * 2;
}
return $perimetersTotal;
}
}
Fatto! Il perimetro di un rettangolo è semplice da calcolare, dato che basta sommare il doppio della larghezza con il doppio dell’altezza.
Tutto procede, quindi, alla grande… se non fosse che il giorno dopo il capo, tutto contento, entra in ufficio e ci dice: “Ottimo lavoro, davvero! Adesso però c’è bisogno di aggiungere la possibilità di calcolare il perimetro dei cerchi. Vero, ci sono tanti rettangoli a questo mondo, ma anche tanti cerchi!”
Possiamo dargli torto? Certo che no.
Una classe PerimeterCalculator ce l’abbiamo già: non dobbiamo fare altro che modificarla e…
Aspetta, ma non è sbagliato modificare le classi per aggiungere delle funzionalità? Dov’è finito quel “una classe deve essere chiusa alla modifica” e tutta quell’altra roba che mi hai detto prima?
Esatto! Progettare il nostro codice così è stato un errore, perché abbiamo analizzato solo un caso base ed anche piuttosto limitato.
Ad ogni modo, proviamo comunque ad effettuare questa modifica e vedere che succede, anche per comprendere meglio le conseguenze di certe scelte.
Creiamo la nostra classe Circle.
radius = $radius;
}
}
Arrivati a questo punto dovremmo necessariamente modificare la classe PerimeterCalculator, inadatta a calcolare un insieme di perimetri di rettangoli e cerchi.
Potremmo quindi fare un semplice controllo, in questo modo:
width * 2 + $shape->height * 2;
}
else
{
$perimetersTotal = 2* $shape->radius * pi();
}
}
return $perimetersTotal;
}
}
La circonferenza di un cerchio, infatti, si calcola moltiplicando il raggio per due, quindi moltiplicando il risultato dell’operazione per pi.
Fin qui tutto bene. O forse no: il capo potrebbe nuovamente entrare in ufficio e chiederci il supporto per un’altra forma geometrica, poi un’altra ancora e così via.
Il risultato? La classe PerimeterCalculator verrebbe modificata di continuo ed esposta a rotture delle funzionalità altrettanto continuamente.
Abbiamo detto che una classe non dovrebbe mai essere modificata, eppure qui stiamo facendo il contrario!
Per trovare una soluzione migliore bisogna iniziare a riflettere sulle competenze di ogni classe. Se ci pensi, la formula per trovare il perimetro può cambiare di forma in forma. Non è una sola, comune. Non a caso, abbiamo dovuto creare una variante per ogni forma geometrica che ci interessava.
Risulta quindi comodo lavorare in modo diverso. Come?
Proviamo a definire una classe astratta, che chiameremo Shape.
width = $width;
$this->height = $height;
}
public function getPerimeter()
{
return $this->width * 2 + $this->height * 2;
}
}
… ed ecco la seconda!
radius = $radius;
}
public function getPerimeter()
{
return $this->radius * 2 * pi();
}
}
Ci siamo quasi: non rimane, infatti, che modificare anche la classe PerimeterCalculator, che ora presenterà una sintassi molto più pulita.
getPerimeter();
}
return $perimetersTotal;
}
}
Visto? Abbiamo creato Circle e Rectangle derivate da Shape (abbiamo aperto all’estensione) e al tempo stesso abbiamo fatto in modo di creare una classe PerimeterCalculator chiusa a qualsiasi modifica. Se ci fai caso, la classe PerimeterCalculator fa tutto quello che deve fare ed è difficile pensare ad un’eventuale modific futura.
Uso delle Interfacce
Probabilmente qualcuno me lo farà giustamente notare, quindi “anticipo” i tempi. Un’obiettivo del genere, in questo caso, può essere raggiunto anche tramite l’uso di interfacce.
Basterebbe creare, infatti, un’interfaccia ShapeInterface…