In questo articolo scopriremo come organizzare il nostro codice in modo da renderlo il più possibile flessibile e resistente alle comuni evoluzioni del software. Tratteremo infatti del secondo dei cinque principi del SOLID: “Software entities should be open for extension, but closed for modification.”.
L'obiettivo dell'Open/Close Principle è:
“ Progettare entità che non necessitino di essere modificate a fronte dei naturali cambiamenti delle nostre applicazioni, ma che possano essere estese con estrema facilità.”
Per capire meglio facciamo subito un esempio: Supponiamo di essere nel pieno dello sviluppo di un software di CMS che potremmo chiamare YACS (Yet Another CMS Solution): per distinguerci un po' dalla massa di prodotti simili abbiamo deciso di creare una dashboard di amministrazione che presenti secondo un particolare ordine tutti i contenuti creati indipendentemente dalla loro tipologia: Pagine, Post, Commenti, Documenti ed Immagini.
Ognuno degli oggetti di cui sopra dovrà essere visualizzato nella dashboard secondo le proprie peculiarità, ad esempio:
Supponendo di avere un oggetto che sia responsabile della gestione della dashboard (DashboardController
), una prima soluzione potrebbe essere quella di creare un metodo con un ciclo che comprenda tutti gli oggetti da stampare e che per ogni iterazione stampi il contenuto di Data, Tipologia, Titolo e Descrizione in accordo con il tipo di oggetto. Ecco lo pseudocodice:
// metodo index per ogni oggetto_da_stampare se è una pagina data = pagina.data_di_creazione tipologia = 'Pagina' titolo = pagina.titolo descrizione = i primi 200 caratteri di pagina.testo ... se è una immagine data = immagine.data_di_upload tipologia = 'Immagine' titolo = immagine.didascalia descrizione = una versione ridimensionata di immagine.src ...
Ora ipotizziamo di voler estendere la dashboard aggiungendo la possibilità selezionare e disabilitare contenuti:
Anche in questo caso il codice che gestisce questa porzione del CMS potrebbe basarsi sulla stessa
logica del precedente:
// metodo disabilitazione_di_massa per ogni oggetto_selezionato se è una pagina pagina.disabilitata = true per ogni documento_legato_alla_pagina documento.disabilitato = true se è un commento commento.disabilitato = true manda una mail a commento.autore informandolo della disabilitazione se è un documento documento.disabilitato = true ...
Perfetto... dove stiamo sbagliando ? Semplice, proviamo a pensare di aggiungere al CMS la
gestione di un nuovo tipo di contenuto, ad esempio Prodotto
, in quanti punti dovremmo
intervenire per incorporare questo nuovo oggetto ?
- nella nuova classe
Prodotto
; - nei metodi
index
edisabilitazione_di_massa
per spiegare alDashboardController
come trattare questi casi con oggetti di tipoProdotto
.
La classe DashBoardController
è decisamente troppo invischiata nella logica degli oggetti che
gestisce!
Ogni cambiamento in uno di questi oggetti o ogni aggiunta di un nuovo oggetto si ripercuote anche su questa classe. Alla lunga una architettura come questa genera un prodotto i cui componenti sono così interdipendenti da non poter più essere riutilizzati per altro, e c'è di più: mantenere un software costruito in questo modo è una vera propria mission impossible, come possiamo sapere in quanti punti una modifica ad uno specifico oggetto debba essere propagata?
Astrazione, comportamenti ed interfacce
La soluzione suggerita dal secondo dei principi SOLID è quella di creare un oggetto che sappia fare
da cuscinetto tra chi deve orchestrare l'azione (DashBoardController
) e chi invece possiede le
informazioni per eseguirla (Pagina
, Post
, ...). Ecco uno schema:
Come si intuisce facilmente stiamo parlando di astrazione: la classe Contenuto
non è altro che il padre di Pagina
, Post
, Commento
, Documento
e Immagine
.
Questa strategia ci consente di definire due metodi astratti sommario
e disabilita
in Contenuto
che poi ognuna delle classi figlie dovrà implementare in accordo con le azioni richieste dallo specifico tipo di contenuto che rappresenta. In questo modo DashBoardController
potrà limitarsi ad invocare il metodo sommario
e disabilita
per ognuno degli oggetti della collezione, senza preoccuparsi della reale operatività di questi metodi:
// metodo index per ogni oggetto_da_stampare dati_per_la_tabella = oggetto_da_stampare.sommario() // metodo disabilitazione_di_massa per ogni oggetto_selezionato oggetto_selezionato.disabilita()
Ottimo diremmo, ma ora stiamo aderendo al secondo principio SOLID ma stiamo violando il primo: supponiamo infatti di voler fare in modo che nell'elenco di oggetti della dashboard compaiano anche gli utenti: sarebbe fantastico se anche la classe Utente potesse beneficiare dei metodi sommario
e disabilita
ma ovviamente non possiamo farla discendere da Contenuto
!
In realtà quello che abbiamo scoperto è che sommario
e disabilita
rappresentano due comportamenti (o behaviors) che possono appartenere anche a classi non necessariamente legate da altri aspetti logici. Possiamo dire che gli oggetti che posseggono queste proprietà siano Riassumibili
e Disabilitabili
.
Da questo deriva che è la sola appartenenza ad uno dei due gruppi a definire se l'oggetto può essere stampato nella dashboard e/o può essere disabilitato.
Esistono modi diversi per raggiungere questo risultato, si possono ad esempio utilizzare delle interfacce che descrivano i metodi che poi dovranno essere implementati dalle classi che vogliono attivarsi sul quel particolare comportamento.
Conclusioni
È difficile ottenere una struttura aderente all'Open Close Principle al 100%, esisteranno sempre aspetti che sfuggiranno al nostro controllo; l'importante è che tali aspetti siano quelli con la minor probabilità di cambiare nel corso del tempo. Anche per il secondo dei 5 principi del SOLID l'esperienza riveste quindi un ruolo decisamente importante.