Decorator pattern: il modello per l’estensione dinamica delle classi
Se volete ampliare le classi già presenti in un software orientato agli oggetti con nuove funzionalità, avete due modi per farlo. La soluzione più facile, ma che può rapidamente complicarsi, riguarda l’implementazione di sottoclassi che estendano le classi base con ciò di cui avete bisogno. L’alternativa riguarda invece l’utilizzo di un’istanza decoratore secondo il cosiddetto decorator design pattern. Questo modello, facente parte dei 23 design pattern della GoF, permette l’estensione dinamica delle classi durante l’esecuzione del software. Senza, tra l’altro, che si venga a creare una gerarchia di ereditarietà infinita e di difficile comprensione.
Di seguito scoprite che cos’è il decorator pattern e quali vantaggi e svantaggi porta con sé. Inoltre, illustriamo il funzionamento del modello avvalendoci di una rappresentazione grafica e di un esempio concreto.
Che cos’è il decorator pattern?
Il decorator design pattern o decorator pattern è un modello pubblicato nel 1994 per un’estensione chiara delle classi nei software orientati agli oggetti. Con questo modello è possibile aggiungere un comportamento desiderato senza influenzare il comportamento degli altri oggetti della stessa classe. Da un punto di vista strutturale, il decorator pattern ricorda molto il chain of responsibility pattern. Nonostante, diversamente da quest’ultimo dotato di un processore centrale, sono le classi a prendere in consegna le richieste.
La componente software che deve essere ampliata viene per così dire “decorata” con una o più classi decorator, che, secondo il principio del decorator pattern, la racchiudono. Ogni decorator è dello stesso tipo della componente posta al suo interno e dispone delle stesse interfacce. In questo modo può delegare senza problemi l’invocazione dei metodi, prima o dopo l’esecuzione del suo comportamento. In teoria anche l’elaborazione diretta di un’invocazione è possibile con il decorator.
A cosa serve il decorator design pattern?
Come per altri modelli GoF, come lo strategy pattern o il builder pattern, il decorator pattern ha l’obiettivo di gestire le componenti dei software orientati agli oggetti in modo che possano essere riutilizzate in maniera più semplice e flessibile. A questo scopo il modello offre la soluzione per poter aggiungere o eliminare le dipendenze a un oggetto in maniera dinamica e durante l’esecuzione, se necessario. Proprio per questo motivo il pattern rappresenta una buona alternativa all’utilizzo delle sottoclassi. Se queste sono infatti in grado di completare una classe in molti modi diversi, non permettono però di apportare modifiche durante l’esecuzione.
Una componente software può essere ampliata da un qualsiasi numero di classi decorator. Tuttavia, queste estensioni rimangono del tutto invisibili per le istanze che vi accedono, tanto che queste non si accorgono che alla classe principale sono state aggiunte delle sottoclassi.
Funzionamento del decorator pattern: diagramma UML
Il decorator e le classi decorator (ConcreteDecorator) dispongono delle stesse interfacce delle componenti software che hanno il compito di decorare (ConcreteComponent) e sono dello stesso tipo. Questo è importante per la gestione delle richieste in quanto serve a stabilire se queste debbano essere inoltrate con o senza modifiche, nel caso in cui non sia il decorator a farsi carico della loro elaborazione. Il decorator pattern definisce questa interfaccia elementare come “component”, teoricamente corrispondente a una superclasse astratta.
La relazione tra la componente base e il decorator può essere meglio illustrata grazie a una rappresentazione grafica sotto forma di diagrammi di classe in UML. Nello schema relativo al decorator design pattern qui sotto abbiamo usato il linguaggio di modellazione grafica per la programmazione orientata agli oggetti.
I vantaggi e gli svantaggi del decorator pattern in sintesi
Considerare l’utilizzo del modello decorator durante l’ideazione di un software risulta ben giustificato per una serie di motivi. Primo di tutto, emerge il grado di flessibilità che ottenete adottando una struttura decorator, la quale permette di ampliare le classi con nuovi comportamenti sia durante la fase di compilazione che durante l’esecuzione. Inoltre, con l’approccio decorator evitate completamente l’ereditarietà e la gerarchia che ne consegue, il che migliora significativamente la leggibilità del codice di programmazione.
Con la suddivisione della funzionalità su più classi decorator migliorano anche le prestazioni del software, ottenendo così di poter richiamare ed eseguire qualsiasi funzione di cui si ha bisogno. Non si ha, invece, questa possibilità di ottimizzazione delle risorse con una classe di base complessa che mette a disposizione tutte le funzioni in modo permanente.
Lo sviluppo basato sul decorator pattern porta con sé anche degli svantaggi. L’inserimento del modello aumenta automaticamente la complessità del software. In particolare, l’interfaccia decorator è solitamente molto carica di scritte oltre a usare molte nuove terminologie, il che la rende tutt’altro che adatta ai principianti.
Un ulteriore svantaggio consiste nell’elevato numero di oggetti decorator, per i quali è consigliabile una sistematizzazione dedicata, per evitare di avere problemi con la strutturazione, come già accade quando si lavora con le sottoclassi. La catena delle chiamate degli oggetti decorati (le componenti software ampliate) appesantiscono inoltre l’individuazione di eventuali errori e l’intero processo di debugging in generale.
Vantaggi | Svantaggi |
---|---|
Elevato grado di flessibilità | Elevata complessità del software, in particolare dell’interfaccia decorator |
Ampliamento della funzionalità delle classi senza ereditarietà | Poco adatto agli utenti alle prime armi |
Codice di programmazione di facile lettura | Elevato numero di oggetti |
Chiamata delle funzioni ottimizzata per le risorse | Processo di debugging appesantito |
Decorator design pattern: gli scenari d’impiego classici
Il decorator pattern offre la base per oggetti estensibili dinamici e trasparenti di un software. In modo particolare questo modello trova applicazione nelle componenti delle interfacce grafiche utente, dette anche GUI. Ad esempio: per dotare un campo di testo di un bordo è sufficiente utilizzare un apposito decorator che viene attivato in maniera “invisibile” tra il campo di testo (l’oggetto) e la chiamata, per aggiungere questo nuovo elemento di interfaccia.
Un esempio molto conosciuto di utilizzo del decorator pattern sono le cosiddette classi stream della libreria Java, responsabili per la gestione degli input e output di dati. Qui, le classi decorator vengono usate specificamente per aggiungere nuove caratteristiche e informazioni di stato al flusso di dati o mettere a disposizione nuove interfacce.
Java non è l’unico linguaggio di programmazione con il quale viene utilizzato il decorator pattern. Anche i seguenti linguaggi impiegano il modello di design:
- C++
- C#
- Go
- JavaScript
- Python
- PHP
Esempio concreto di applicazione del decorator pattern
Il precedente elenco di vantaggi e svantaggi dimostra che il decorator design pattern non è adatto a qualunque tipo di software. Tuttavia, laddove si necessita di apportare modifiche a posteriori a una classe, questo modello di design rappresenta una soluzione di prim’ordine. In un articolo del suo blog ZKMA, Marcel Schöni fornisce un buon esempio d’impiego delle classi “decorate”.
La situazione di partenza da lui descritta è quella di un software che serve a consultare i nomi di persone attraverso una classe astratta denominata “dipendente”. La prima lettera del nome cercato dall’utente è sempre minuscola. Non essendo possibile apportare una modifica a posteriori, viene implementata la classe decorator “DipendenteDecorator”, che opera tramite la stessa interfaccia e che rende possibile l’utilizzo del metodo getName(). Inoltre, il decorator viene dotato di una logica che assicura che la prima lettera sia scritta con la maiuscola. Il codice di esempio appare così:
public class DipendenteDecorator implements Person {
private Dipendente dipendente;
public DipendenteDecorator(Dipendente dipendente){
this.dipendente = dipendente;
}
public String getName(){
// invoca il metodo della classe Dipendente
String name = dipendente.getName();
// assicura che la prima lettera sia maiuscola
name = Character.toUpperCase(name.charAt(0))
+ name.substring(1, name.length());
return name;
}
}