Open / Closed, secondo principio solid
Le entità software (classi, moduli, funzioni, ecc.) dovrebbero essere aperte per l’estensione, ma chiuse per la modifica.
Il secondo principio solid open closed, ci da un’indicazione precisa sulla buona tenuta del software in fase di manutenzione.
Come fare ?, vediamolo insieme: in questo articolo vediamo il principio solid open closed, con esempi
Tempo di lettura stimato: 7 minuti
Il primo principio solid ci dice come progettare il software: moduli, classi e funzioni in modo tale che quando è necessaria una nuova funzionalità, non dovremmo modificare il codice esistente ma piuttosto scrivere nuovo codice che verrà utilizzato dal codice esistente. Questo potrebbe sembrare strano, soprattutto con linguaggi come Java, C, C ++ o C# dove si applica non solo al codice sorgente stesso ma anche al binario. Vogliamo creare nuove funzionalità in modi che non richiedano la ridistribuzione di file binari, eseguibili o DLL esistenti. OCP nel contesto S.O.L.I.D.
SRP e OCP complementari
Abbiamo già visto il principio SRP della Responsabilità Unica che afferma che un modulo dovrebbe avere un solo motivo per cambiare. I principi OCP e SRP, sono complementari. Il codice progettato seguendo il principio SRP, rispetterà anche i principi OCP. Quando abbiamo un codice che ha una sola ragione per cambiare, l’introduzione di una nuova funzionalità creerà una ragione secondaria per quel cambiamento. Quindi sia SRP che OCP verrebbero violati. Allo stesso modo, se abbiamo un codice che dovrebbe cambiare solo quando cambia la sua funzione principale e dovrebbe rimanere invariato quando viene aggiunta una nuova funzionalità, rispettando così l’OCP, rispetterà principalmente anche SRP.
Ciò non significa che SRP porti sempre a OCP o viceversa, ma nella maggior parte dei casi se uno di essi viene rispettato, raggiungere il secondo è abbastanza semplice.
Esempio di violazione del principio solid open closed
Da un punto di vista puramente tecnico, il Principio solid Open / Closed è molto semplice. Una semplice relazione tra due classi, come quella sotto, viola il principio OCP.
La classe User utilizza direttamente la classe Logic. Se abbiamo bisogno di implementare una seconda classe Logic in un modo che ci consenta di usare sia quella attuale che quella nuova, la classe Logic esistente dovrà essere cambiata. L’utente è direttamente legato all’implementazione della logica, non c’è modo per noi di fornire una nuova logica senza influire su quella attuale. E quando parliamo di linguaggi tipizzati staticamente, è molto probabile che anche la classe User richieda modifiche. Se parliamo di linguaggi compilati, sicuramente sia l’eseguibile User che l’eseguibile Logic o la libreria dinamica richiederanno la ricompilazione e il delivery, preferibile evitare quando possibile.
Con riferimento allo schema precedente, possiamo dedurre che qualsiasi classe che utilizza direttamente un’altra classe, potrebbe portare alla violazione del principio solid Open/Closed.
Esempio
Supponiamo di voler scrivere una classe in grado di fornire lo stato di avanzamento “in percentuale” di un file scaricato, tramite la nostra applicazione. Avremo due classi principali, una Progress e una File, e immagino che vorremo usarle come segue:
function testItCanGetTheProgressOfAFileAsAPercent() {
$file = new File();
$file->length = 200;
$file->sent = 100;
$progress = new Progress($file);
$this->assertEquals(50, $progress->getAsPercent());
}
In questo codice siamo utenti di Progress. Vogliamo ottenere un valore come percentuale, indipendentemente dalla dimensione effettiva del file. Usiamo File come fonte di informazioni. Un file ha una lunghezza in byte e un campo chiamato sent che rappresenta la quantità di dati inviati a chi esegue il download. Non ci interessa come questi valori vengono aggiornati nell’applicazione. Possiamo presumere che ci sia una logica magica che lo fa per noi, quindi in un test possiamo impostarli esplicitamente.
class File {
public $length;
public $sent;
}
La classe File è solo un semplice oggetto dati contenente i due campi. Ovviamente dovrebbe contenere anche altre informazioni e comportamenti, come nome file, percorso, percorso relativo, directory corrente, tipo, permessi e così via.
class Progress {
private $file;
function __construct(File $file) {
$this->file = $file;
}
function getAsPercent() {
return $this->file->sent * 100 / $this->file->length;
}
}
Progress è semplicemente una classe che accetta un file nel suo costruttore. Per chiarezza, abbiamo specificato il tipo di variabile nei parametri del costruttore. C’è un unico metodo utile su Progress, getAsPercent(), che prenderà i valori inviati e la lunghezza da File e li trasformerà in una percentuale. Semplice e funziona.
Questo codice sembra essere corretto, tuttavia viola il principio solid Open / Closed.
Ma perché?
E come?
Proviamo a cambiare i requisiti
Ogni applicazione per evolversi nel tempo avrà bisogno di nuove funzionalità. Una nuova funzionalità per la nostra applicazione potrebbe essere quella di consentire lo streaming di musica, invece di scaricare solo i file. La lunghezza del file è rappresentata in byte, la durata della musica in secondi. Vogliamo offrire una barra di avanzamento ai nostri ascoltatori, ma possiamo riutilizzare la classe scritta sopra ?
No, non possiamo. La nostra progressione è vincolata a File. E’ in grado di gestire solo le informazione dei file, anche se può essere applicato anche a contenuti musicali. Ma per fare ciò dobbiamo modificarlo, dobbiamo fare in modo che Progress conosca musica e file. Se il nostro design rispettasse il principio solid open closed, non avremmo bisogno di toccare File o Progress. Potremmo semplicemente riutilizzare il progresso esistente e applicarlo alla musica.
Possibile soluzione
I linguaggi tipizzati dinamicamente hanno il vantaggio di gestire i tipi di oggetti in fase di esecuzione. Questo ci consente di rimuovere il typehint dal costruttore di Progress e il codice continuerà a funzionare.
class Progress {
private $file;
function __construct($file) {
$this->file = $file;
}
function getAsPercent() {
return $this->file->sent * 100 / $this->file->length;
}
}
Ora possiamo lanciare qualsiasi cosa a Progress. E con qualsiasi cosa, intendo letteralmente qualsiasi cosa:
class Music {
public $length;
public $sent;
public $artist;
public $album;
public $releaseDate;
function getAlbumCoverFile() {
return 'Images/Covers/' . $this->artist . '/' . $this->album . '.png';
}
}
E la class Music come quella sopra funzionerà perfettamente. Possiamo testarla facilmente con un test molto simile a File.
function testItCanGetTheProgressOfAMusicStreamAsAPercent() {
$music = new Music();
$music->length = 200;
$music->sent = 100;
$progress = new Progress($music);
$this->assertEquals(50, $progress->getAsPercent());
}
Quindi, in pratica, qualsiasi contenuto misurabile può essere utilizzato con la classe Progress. Forse dovremmo esprimerlo in codice cambiando anche il nome della variabile:
class Progress {
private $measurableContent;
function __construct($measurableContent) {
$this->measurableContent = $measurableContent;
}
function getAsPercent() {
return $this->measurableContent->sent * 100 / $this->measurableContent->length;
}
}
Quando abbiamo specificato File come typehint, eravamo ottimisti su ciò che la nostra classe può gestire. Era esplicito e se fosse arrivato qualcos’altro, un bell’errore ce lo diceva.
Una classe che sovrascrive un metodo di una classe base in modo tale che il contratto della classe base non venga rispettato dalla classe derivata.
Non vogliamo finire per provare a chiamare metodi o accedere a campi su oggetti non conformi al nostro contratto. Quando abbiamo avuto un typehint, il contratto è stato specificato da esso. I campi e i metodi della classe File. Ora che non abbiamo nulla, possiamo inviare qualsiasi cosa, anche una stringa e risulterebbe in un brutto errore.
Risultato finale dell’applicazione del principio solid Open Closed
Mentre il risultato finale è lo stesso in entrambi i casi, il che significa che il codice si rompe, il primo ha prodotto un bel messaggio. Questo, tuttavia, è molto oscuro. Non c’è modo di sapere quale sia la variabile – una stringa nel nostro caso – e quali proprietà sono state cercate e non trovate. È difficile eseguire il debug e risolvere il problema. Un programmatore deve aprire la classe Progress, leggerla e comprenderla. Il contratto, in questo caso, quando non si specifica esplicitamente il typehint, è definito dal comportamento di Progress. È un contratto implicito, noto solo a Progress. Nel nostro esempio, è definito dall’accesso ai due campi, sent e length, nel metodo getAsPercent(). Nella vita reale il contratto implicito può essere molto complesso e difficile da scoprire guardando solo per pochi secondi in classe.
Questa soluzione è consigliata solo se nessuno degli altri suggerimenti di seguito può essere facilmente implementato o se infliggerebbero seri cambiamenti architettonici che non giustificano lo sforzo.
Continua leggendo il terzo principio della sostituzione di Liskow —>