[09] Patterns
- Patterns
Patterns
Parlando di progettazione del software e di buone pratiche è impossibile non parlare di design patterns, soluzioni universalmente riconosciute valide a problemi di design ricorrenti: si tratta cioè di strumenti concettuali di progettazione che esprimono un’architettura vincente del software catturando la soluzione ad una famiglia di problemi.
Ad ogni pattern sono associati una serie di idiomi, implementazioni del pattern specifiche per un certo linguaggio di programmazione che sfruttano i costrutti del linguaggio per realizzare l’architettura dettata dal pattern. Durante questa discussione vedremo alcuni idiomi per Java, che talvolta si discosteranno fortemente dalla struttura descritta dai diagrammi UML dei pattern.
Ma attenzione, esistono anche degli anti-pattern, soluzioni che sembrano buone ma sono dimostratamente problematiche: dovremo assicurarci di tenerci lontani da questi design truffaldini!
Discutere di pattern: i meta-pattern
Prima di iniziare a parlare dei principali pattern che un informatico dovrebbe conoscere, possiamo chiederci come possiamo parlare di pattern: semplice, con dei meta-patterns, pattern con cui costruire altri pattern!
Nello specifico, i meta-patterns identificano due elementi base su cui ragionare quando si trattano i pattern:
-
HookMethod: un “metodo astratto” che, implementato, determina il comportamento specifico nelle sottoclassi; è il punto caldo su cui interveniamo per adattare lo schema alla situazione.
-
TemplateMethod: metodo che coordina generalmente più HookMethod per realizzare il design voluto; è l’elemento freddo di invariabilità del pattern che ne realizza la rigida struttura.
Ovviamente i metodi template devono avere un modo per accedere ai metodi hook se intendono utilizzarli per realizzare i pattern. Tale collegamento può essere fatto in tre modi differenti:
- Unification: hook e template si trovano nella stessa classe astratta, classe da cui erediteranno le classi concrete per implementare i metodi hook e, di conseguenza, il pattern; i metodi template sono invece già implementati in quanto la loro struttura non si deve adattare alla specifica applicazione.
- Connection: hook e template sono in classi separate, indicate rispettivamente come hook class (astratta) e template class (concreta), collegate tra di loro da un’aggregazione: la classe template contiene cioè un’istanza della classe hook, in realtà un’istanza della classe concreta che realizza i metodi hook usati per implementare il pattern.
- Recursive connection: come nel caso precedente hook e template sono in classi separate, ma oltre all’aggregazione tali classi sono qui legate anche da una relazione di generalizzazione: la classe template dipende infatti dalla classe hook.
Vedremo a quale meta-pattern aderiranno i pattern che vediamo. A tal proposito,i pattern che vedremo fanno parte dei cosiddetti “Gang Of Four patterns”, una serie di 23 pattern definiti da Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides; oltre ad averli definiti, questi signori hanno diviso i pattern in tre categorie:
- Creazionali: legati alla creazione di oggetti
- Comportamentali: legati all’interazione tra oggetti
- Strutturali: legati alla composizioni di classi e oggetti
SINGLETON
Talvolta vorremmo che di un certo oggetto esistesse una sola istanza perché logicamente di tale oggetto non ha senso esistano diverse copie all’interno dell’applicazione (es. diverse istanze della classe Gioco in un sistema che gestisce un solo gioco alla volta). Tuttavia i linguaggi Object-Oriented gestiscono solo classi con istanze multiple, per cui la realizzazione di questa unicità può rivelarsi più complessa del previsto.
La soluzione consiste nel rendere la classe stessa responsabile del fatto che non può esistere più di una sua istanza: per fare ciò il primo passo è ovviamente quello di rendere privato il costruttore, o se non privato comunque non pubblico (conviene metterlo protected in modo da poter creare sottotipi).
Bisogna però garantire comunque un modo per recuperare l’unica istanza disponibile della classe: si crea dunque il metodo statico getInstance
che restituisce a chi lo chiama l’unica istanza della classe, creandola tramite il costruttore privato se questa non è già presente.
Tale istanza è infatti memorizzata in un attributo statico della classe stessa, in modo così da poterla restituire a chiunque ne abbia bisogno.
Con queste accortezze è possibile creare una classe Singleton simile a questa:
public class Singleton {
/* costruttore privato o comunque non pubblico */
protected Singleton() { ... }
/* salvo l'istanza per usarla dopo */
private static Singleton instance = null;
/* metodo statico */
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
public void metodoIstanza() { ... }
}
Tuttavia, per come lo abbiamo scritto questa classe non assicura di non creare più di un’istanza di sé stessa, in quanto non prende in considerazione la concorrenza.
Se due processi accedono in modo concorrente al metodo getInstance
, entrambi potrebbero eseguire il controllo sul valore nullo dell’istanza ed ottenere un successo in quanto l’istanza non è ancora stata assegnata al relativo attributo statico nell’altro processo: si ottiene dunque che uno dei due processi ha accesso ad una propria istanza privata, cosa che distrugge completamente il nostro pattern!
Una prima soluzione sarebbe di mettere un lock sull’esecuzione del metodo anteponendovi la direttiva @Synchronized
: tuttavia, tale approccio comporterebbe un notevole calo di prestazioni del sistema portando vantaggi unicamente alla prima chiamata.
Una soluzione molto più efficiente (non possibile però fino a Java 5) è invece quella che prevede di avere un blocco sincronizzato di istruzioni posto all’interno del ramo in cui si pensa che l’istanza sia nulla in cui ci si chiede se effettivamente l’istanza è nulla e solo allora si esegue il costruttore; la presenza del doppio controllo assicura che non vi siano squilibri dovuti alla concorrenza, mentre sincronizzare solamente un blocco e non l’intero metodo fa sì che il calo di prestazioni sia sentito solamente durante le prime chiamate concorrenti.
Idioma Java
Fortunatamente si è sviluppato per il linguaggio Java un idioma molto semplice per il Singleton, in cui al posto di usare una classe per definire l’oggetto si usa un enumerativo con un unico valore, l’istanza.
Ciascun valore di tali oggetti è infatti trattato nativamente da Java proprio come un Singleton: viene creato al momento del suo primo uso, non ne esiste più di una copia, e chiunque vi acceda accede sempre alla medesima istanza.
La possibilità di creare attributi e metodi all’interno degli enum
completa il quadro.
public enum MySingleton {
INSTANCE;
public void metodoIstanza() { ... }
}
MySingleton.INSTANCE.sampleOp();
Si tratta inoltre di un approccio “thread safe”, ovvero che lavora già bene con la concorrenza; l’unico svantaggio è che, se non si conosce l’idioma, a prima vista questa soluzione risulta molto meno chiara rispetto all’approccio precedente.
ITERATOR
Talvolta gli oggetti che definiamo fanno da aggregatori di altri oggetti, contenendo cioè una collezione di questi su cui poi fare particolari operazioni: in questi casi è molto probabile che vorremo poter iterare sui singoli elementi aggregati, ma senza esporre la rappresentazione interna usata per contenerli.
Proprio per risolvere questo tipo di problematiche nasce il pattern Iterator: esso consiste nella creazione di una classe ConcreteIterator
che abbia accesso alla rappresentazione interna del nostro oggetto e esponga i suoi elementi in modo sequenziale tramite i metodi next()
e hasNext()
; dovendo accedere alla rappresentazione, molto spesso tale iteratore si realizza come una classe interna anonima.
Java supporta largamente il pattern Iterator, a tal punto che nella libreria standard esiste un’interfaccia generica per gli iteratori, Iterator<E>
: all’interno di tale interfaccia sono definiti, oltre ai metodi di cui sopra, il metodo remove()
, normalmente non supportato in quanto permetterebbe di modificare la collezione contenuta dalla classe, e il metodo forEachRemaining()
, che esegue una data azione su tutti gli elementi ancora non estratti dell’iteratore.
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() {
throw new UnsupportedOperationException("remove");
}
/* aggiunta funzionale opzionale */
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}
Esiste inoltre un’interfaccia che l’oggetto iterabile può implementare, Iterable<E>
: essa richiede solamente la presenza di un metodo iterator()
che restituisca l’iteratore concreto, e una volta implementata permette di utilizzare il proprio oggetto aggregatore all’interno di un costrutto foreach.
Così, per esempio, possiamo passare dal seguente codice:
Iterator<Card> cardIterator = deck.getCards();
while (cardIterator.hasNext()) {
Card card = cardIterator.next();
System.out.println(card.getSuit());
}
… a quest’altro:
for (Card card : deck) {
System.out.println(card.getSuit());
}
Oltre ad essere più stringato il codice è significativamente più chiaro, rendendo palese che la singola card
sia read-only.
CHAIN OF RESPONSIBILITY
Talvolta nei nostri programmi vorremmo definire una gestione “a cascata” di una certa richiesta. Pensiamo per esempio a una serie di regole anti-spam: all’arrivo di una mail la prima regola la esamina e si chiede se sia applicabile o meno; in caso affermativo contrassegna la mail come spam, altrimenti la passa alla prossima regola, che a sua volta farà lo stesso test passando il controllo alla terza in caso negativo, e così via. Abbiamo cioè un client in grado di fare una richiesta, e una catena di potenziali gestori di cui non sappiamo a priori chi sarà in grado di gestirla effettivamente.
Il pattern Chain of Responsibility risolve il disaccoppiamento tra client e gestore concatenando i gestori.
Esso prescrive la creazione di un’interfaccia a cui tutti i gestori devono aderire, contenente solo la dichiarazione di un metodo evaluate
che implementa la logica descritta prima: si stabilisce se si può gestire la richiesta, e se non si può si chiama lo stesso metodo su un altro gestore ottenuto come parametro al momento della creazione.
In questo modo all’interno del client è sufficiente creare una vera e propria catena di gestori e chiamare il metodo evaluate
del primo: si noti che l’ordine in cui vengono assemblati tali gestori conta, in quanto la valutazione procede sequenzialmente.
public interface Gestore {
/* Il tipo di ritorno dipende dal campo applicativo */
public ??? evaluate();
}
public class Client {
private Gestore evaluator =
new GestoreConcreto1(
new GestoreConcreto2(
new GestoreConcreto3(null)));
public void richiesta() {
evaluator.evaluate();
}
}
FLYWEIGHT
Talvolta ci troviamo in una situazione simile a quella che aveva ispirato il pattern Singleton: abbiamo una serie di oggetti immutabili fortemente condivisi all’interno del programma e per motivi di performance e risparmio di memoria vorremmo che non esistano istanze diverse a parità di stato. Se due client devono usare un’istanza con lo stesso stato vorremmo cioè non usino ciascuno un’istanza duplicata ma proprio la stessa istanza: essendo le istanze immutabili, tale condivisione non dovrebbe infatti creare alcun tipo di problema.
Il pattern FlyWeight serve a gestire una collezione di oggetti immutabili assicurandone l’unicità: esso consiste nel rendere privato il costruttore e costruire tutte le istanze a priori con un costruttore statico, salvandole in una lista privata.
I client possono dunque richiedere una certa istanza con un metodo get
specificando lo stato dell’istanza desiderata: in questo modo, a parità di richiesta verranno restituite le stesse identiche istanze.
Abbiamo visto un’applicazione di questo pattern durante i laboratori parlando di Card
:
public class Card {
private static final Card[][] CARDS = new Card[Suit.values().length][Rank.values().length];
static {
for (Suit suit : Suit.values()) {
for (Rank rank : Rank.values()) {
CARDS[suit.ordinal()][rank.ordinal()] = new Card(rank, suit);
}
}
}
public static Card get(Rank pRank, Suit pSuit) {
return CARDS[pSuit.ordinal()][pRank.ordinal()];
}
}
A differenza del pattern Singleton è difficile definire a priori quante istanze ci sono: abbiamo un’istanza per ogni possibile combinazione dei valori degli attributi che compongono lo stato. Proprio per questo motivo il pattern può risultare un po’ inefficiente per oggetti con rappresentazioni grandi: alla prima computazione vengono infatti inizializzati tutti gli oggetti, perdendo un po’ di tempo e sprecando potenzialmente spazio se non tutte le istanze saranno accedute.
NULLOBJECT
Spesso nei nostri programmi avremo bisogno di utilizzare valori “nulli”: pensiamo per esempio al termine di una Chain of Responsibilities, dove per fermare la catena di chiamate dobbiamo dare un valore nullo al next
dell’ultimo gestore.
In generale, a una variabile che indica un riferimento ad un oggetto possiamo assegnare il valore speciale null
per indicare che essa non punta a nulla.
Il problema sorge però quando a runtime si prova a dereferenziare tale valore e viene sollevata un’eccezione (NullPointerException
in Java): questa possibilità ci costringe nel codice ad essere sempre molto titubanti sui valori che ci vengono passati, in quanto non possiamo mai assumere che essi puntino ad un valore reale e dunque dobbiamo sempre controllare che non siano nulli.
C’è però da dire che anche con tali accortezze l’utilizzo di null
è poco carino, in quanto un valore nullo può indicare cose anche molto diverse:
- un errore a runtime
- uno stato temporaneamente inconsistente
- un valore assente o non valido
Ogni volta che si utilizza null
il codice diventa un po’ meno chiaro, e sarebbe necessario disambiguare con commenti o documentazione per spiegare con che accezione tale valore viene usato.
Anche le strategie di gestione del null
variano drasticamente a seconda del significato assegnato a tale valore: quando non ci sono valori “assenti” e dunque il null
indica solo un errore è sufficiente controllare che i dati passati non siano nulli con condizioni, asserzioni o l’annotazione @NotNull
.
Quando invece ci sono valori “assenti”, ovvero che indicano situazioni particolari (es. il Joker in un mazzo di carte, che non ha né Rank né Suit), la gestione è più complicata.
Se non vogliamo trattarli come null
per l’ambiguità che tale valore introduce, un’altra opzione è creare un metodo booleano nella classe che restituisce se l’istanza ha il valore nullo (es. isJoker()
): tuttavia, questo apre le porte a errori da parte dell’utente, che potrebbe dimenticarsi di fare tale controllo e usare l’oggetto come fosse qualunque altro.
Per creare un oggetto che corrisponda al concetto di nessun valore o valore neutro nasce allora il pattern NullObject: si crea all’interno della classe o dell’interfaccia un oggetto statico chiamato NULL
che fornisce particolari implementazioni dei metodi della stessa per realizzare l’idea di valore nullo a livello di dominio.
In questo modo tale oggetto mantiene l’identità della classe rimanendo però sufficientemente separato dagli altri valori; inoltre, la presenza di implementazioni specifiche dei metodi evita il lancio di eccezioni ambigue.
public interface CardSource {
Card draw();
boolean isEmpty();
public static CardSource NULL = new CardSource() {
public boolean isEmpty() {
return true;
}
public Card draw() {
assert !isEmpty();
return null;
}
}
}
Quindi possiamo notare che il concetto del NullObject pattern è quello di creare un oggetto in cui viene definito un comportamento specifico per ogni metodo che rispecchia ciò che accadrebbe nel caso in cui il metodo venisse chiamato su null nel normale flusso di istruzioni.
STRATEGY / DELEGATION
Talvolta nelle nostre classi vogliamo definire comportamenti diversi per diverse istanze: la soluzione classica dei linguaggi Object-Oriented è la creazione di una gerarchia di classi in cui le classi figlie sovrascrivano i metodi della classe genitore.
Tuttavia, questo espone a delle problematiche: cosa fare se per esempio la classe genitore cambia aggiungendo un metodo che una delle classi figlie non dovrebbe poter implementare (es. RubberDuck
come figlia di Duck
, che aggiunge il metodo fly()
)?
Non volendo violare il principio Open-Close, non siamo intenzionati a rimuovere il metodo incriminato, per cui dobbiamo cercare altre soluzioni. Una prima idea sarebbe quella di sopperire al fatto che la classe genitore non sappia chi sono i suoi figli con costrutti proprietari del linguaggio:
- una classe
Final
non permette di ereditare, ma questo non ci permetterebbe di differenziare il comportamento; - una classe
Sealed
(aggiunta di Java 17) sceglie esplicitamente chi possano essere i suoi figli: in questo modo si può evitare che la classe figlia problematica non possa ereditare, ma si tratta comunque di una soluzione parziale.
Non si può neanche pensare di fare semplicemente l’override nella classe figlia del metodo aggiunto facendo in modo che lanci un’eccezione: si avrebbe infatti una inaccettabile violazione del principio di sostituzione di Liskov, che afferma sostanzialmente che un’istanza di una sottoclasse deve poter essere usata senza problemi al posto dell’istanza di una classe genitore.
Una soluzione migliore si basa invece sul concetto di delega, che sostituisce all’ereditarietà la composizione.
Fondamentalmente si tratta di individuare ciò che cambia nell’applicazione e separarlo da ciò che rimane fisso: si creano delle interfacce per i comportamenti da diversificare e una classe concreta che implementa ogni diverso comportamento possibile.
All’interno della classe originale si introducono dunque degli attributi di comportamento, impostati al momento della costruzione o con dei setter a seconda della dinamicità che vogliamo permettere: quando viene richiesto il comportamento a tale classe essa si limiterà a chiamare il proprio “oggetto di comportamento”.
Nell’esempio delle Duck
, per esempio, la struttura è la seguente:
Come si vede, qui non c’è scritto da nessuna parte che una Duck
deve volare, ma solo che deve definire la sua “politica di volo” incorporando un FlyBehaviour
.
La differenziazione dei comportamenti si fa dunque a livello d’istanza e non di classe: il pattern definisce una famiglia algoritmi e li rende tra di loro intercambiabili tramite encapsulation.
Per questo motivo tale pattern è usato in situazioni anche molto diversa da quella con cui l’abbiamo introdotto: un altro esempio presente in Java è l’interfaccia Comparator
.
OBSERVER
Molto spesso capita di avere nei nostri programmi una serie di elementi che vanno tenuti sincronizzati: pensiamo per esempio ad una ruota dei colori che deve aggiornare i valori RGB quando l’utente seleziona un punto con il mouse. Abbiamo cioè uno stato comune che va mantenuto coerente in tutti gli elementi che lo manipolano.
Nella realizzazione di questa funzionalità si rischia di cadere nell’anti-pattern delle pairwise dependencies in cui ogni vista dello stato deve conoscere tutte le altre: si ha cioè un forte accoppiamento e una bassissima espandibilità, in quanto per aggiungere una vista dobbiamo modificare tutte le altre. Ovviamente basta avere poco più di due diverse viste perché il numero di dipendenze (e dunque di errori) cresca esponenzialmente: questo anti-pattern è proprio tutto il contrario del principio di separazione, che predicava forte coesione interna e pochi accoppiamenti esterni.
La soluzione proposta dal pattern Observer è dunque quella di estrarre la parte comune (lo stato) e isolarlo in un oggetto a parte, detto Subject: tale oggetto verrà osservato da tutte le viste, le cui classi prendono ora il nome di Observer.
Si sta cioè centralizzando la gestione dello stato: abbiamo cioè \(n\) classi che osservano una classe centrale e reagiscono ad ogni cambiamento di stato di quest’ultima.
Si tratta una situazione talmente comune che in Java erano presenti delle classi (ora deprecate in quanto non thread-safe) per realizzare questo pattern: java.util.Observer
e java.util.Observable
.
Ma come fanno gli Observer a sapere che il Subject è cambiato? L’idea di fare un continuo polling (chiedo “Sei cambiato?” al Subject), non è ovviamente sensata, in quanto bloccherebbe l’esecuzione sprecando tantissime risorse. Invertiamo invece la responsabilità con un’architettura event-driven: gli Observer si registrano al Subject, che li informerà quando avvengono cambiamenti di stato.
Restano però da capire un paio di cose.
Bisogna innanzitutto spiegare come colleghiamo Observer e Subject: come si vede in figura, esiste una classe Observable
che funge da base da cui ereditare per ogni Subject; vi è poi un’interfaccia Observer
che gli Observer concreti devono ovviamente implementare.
A questo punto gli Observer si possono sottoscrivere al Subject semplicemente attraverso l’uso delle sue funzioni addObserver()
e removeObserver()
, venendo così sostanzialmente inseriti o rimossi nella lista interna degli Observer interessati.
Una volta che lo stato del Subject viene cambiato, solitamente attraverso una serie di metodi pubblici che permettano a tutti di modificarlo (setState()
), esso chiama dunque il suo metodo notifyObservers()
: questo altro non fa che ciclare su tutti gli Observer sottoscritti chiamandone il metodo update(Observable, Object)
, dove:
-
Observable
è il Subject di cui è stato modificato lo stato (l’uso di interfacce permette di sottoscrivere un Observer a più Subject tra cui disambiguare al momento dell’update) -
Object
è la parte di stato che è cambiata (Object perché il tipo dipende ovviamente dal Subject in questione)
Sul metodo di notifica del cambiamento di stato esistono però due diverse filosofie, push e pull, ciascuna con i suoi campi applicativi prediletti: vediamole dunque singolarmente, evidenziando quando e come esse sono utilizzate.
push
In questo caso l’argomento Observable di update
viene messo nullo, mentre nell’Object viene passata la totalità dello stato del Subject:
// Observable
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(null, state);
}
}
// Observer
@Override
public void update(Observable model, Object state) {
if (state instanceof Integer intValue) {
doSomethingOn(intValue);
}
}
Come si vede, dovendo definire come reagire al cambiamento di stato in update
l’Observer dovrà innanzitutto fare un down-casting per ottenere un oggetto della classe corretta.
Avendo la responsabilità di tale casting l’Observer dovrà conoscere precisamente la struttura dello stato del Subject, creando una forte dipendenza che potrebbe creare problemi di manutenibilità.
Un altro problema di questo approccio è che gli Observer sono solitamente interessati a una piccola porzione dello stato del Subject, quindi passarlo tutto come parametro potrebbe sovraccaricare inutilmente la memoria.
pull
Con questo approccio, invece di mandare lo stato all’update
viene passato il Subject stesso, il quale conterrà uno o più metodi per accedere allo stato (getState
):
// Observable
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(this, null);
}
}
// Observer
@Override
public void update(Observable model, Object state) {
if (model instanceof ConcreteObservable cModel) {
doSomethingOn(cModel.getState());
}
}
Sebbene comporti un passaggio in più poiché l’Observer deve chiamare un metodo del Subject quando riceve la notifica, questo cambio di prospettiva offre due vantaggi: in primo luogo non viene passato tutto lo stato, il che fa risparmiare molta memoria; inoltre, il Subject potrebbe decidere di rendere disponibili sottoinsiemi diversi dello stato con getter diversi, mostrando così ad ogni Observer solo le informazioni per esso rilevanti.
Inoltre, sebbene anche in questo caso sia richiesto un casting (da Observable al Subject), questo approccio rende meno dipendenti dalla rappresentazione interna del Subject: fintanto che la firma dei getter non cambia lo stato interno del Setter può cambiare senza problemi.
Approccio ibrido e dipendenze
Partiamo col dire che molto spesso nei casi reali gli approcci push e pull sono ibridati tra di loro: ad update
viene passato sia il Subject che quella parte di stato utile a tutti gli Observer, mentre qualora gli serva qualcosa di più specifico essi se lo andranno a prendere con il getter.
Il vero problema di entrambi gli approcci è però quello delle dipendenze: nel caso push dipendiamo dalla rappresentazione interna del Subject, mentre nel caso pull dalla sua classe concreta. Poiché tale dipendenza non è facilmente eliminabile, piuttosto che lasciarla nascosta nel casting conviene esplicitarla:
-
all’interno dell’Observer salvo l’istanza di Observable a cui mi sono sottoscritto, così al momento dell’
update
posso verificare direttamente che l’istanza sia quella al posto di fare un casting; -
creiamo una classe
State
e l’aggreghiamo sia nell’Observer che nell’Observable concreto in modo che essa nasconda la rappresentazione reale dello stato.
Otteniamo dunque un codice simile al seguente:
public class State { /* rappresentazione interna dello stato */ }
public class Observable {
private State stato;
private List<Observer> observers = new ArrayList<>();
public void addObserver(@NotNull Observer obs) { observers.add(obs); }
public void removeObserver(@NotNull Observer obs) { observers.remove(obs); }
public void notifyObservers() {
for (Observer obs: observers) update(this, stato);
}
}
public class Subject extends Observable {
public void setState(State nuovoStato) { ... }
public State getState() { return super.stato; }
/* Opzionale: altri metodi getter */
}
public interface Observer {
public void update(Observable subject, Object stato);
}
public class ConcreteObserver {
private Observable mySubject;
@Override
public void update(Observable subject, Object stato) {
if (subject == mySubject) {
...
}
}
}
ADAPTER
Spesso nei programmi che scriviamo capita di dover far collaborare interfacce diverse di componenti non originariamente sviluppati per lavorare insieme. Questo capita in una miriade di situazioni, ma volendone citare alcune:
- in un ambito di sviluppo COTS (Component Off The Shelf: sviluppiamo solo ciò che non è disponibile tramite librerie o codice open-source) riutilizziamo tanti componenti presi dal mercato, non pensati per essere compatibili;
- sviluppando ed evolvendo un programma in modo incrementale capita di dover integrare componenti nuovi con componenti vecchi (legacy) per garantire una certa continuità nell’esperienza utente.
Da tutta una serie di situazioni simili è nato il bisogno di creare delle strutture che permettessero di rendere compatibili componenti già esistenti, ovvero creare della “colla” in grado di legare i componenti tra loro per soddisfare le specifiche del sistema. È così ben presto scaturito il pattern Adapter, un pattern ormai molto diffuso che consiste nel creare vari moduli che possano essere incollati o adattati ad altre strutture in modo da renderle utilizzabili incrementalmente e in modo controllato.
Sebbene sia già utilizzato molto spesso, talvolta anche inconsciamente, approfondiamo il pattern in questa sede non solo per imparare a usarlo con più criterio, ma anche perché di esso esistono due “versioni”:
- Class Adapter: adatta una classe.
- Object Adapter: adatta un oggetto di una classe.
Come vedremo, questi due pattern sono molto simili a livello di schema UML ma abbastanza differenti da rendere importante capire quale usare in quali contesti, comprendendo vantaggi e svantaggi di entrambi.
Class Adapter
Come si vede dallo schema UML, per permettere a un Client di comunicare tramite un’interfaccia Target con un componente concreto vecchio detto Adaptee il Class Adapter utilizza una classe concreta che implementa l’interfaccia Target e estende la classe Adaptee, ereditandone così i metodi e la vecchia interfaccia: all’interno di tale classe potremo dunque limitarci a rimappare le funzionalità richieste dalla nuova interfaccia su quella vecchia, implementando qualcosa solo se strettamente necessario e comunque sfruttando la logica già presente della classe estesa.
public class Adapter extends Adaptee implements Target {
@Override
public void request() {
this.oldRequest();
}
}
In questo modo il client utilizzerà l’adapter come se fosse l’oggetto completo, non accorgendosi che quando ne chiama un metodo in realtà il codice eseguito è quello appartenente alla vecchia classe già esistente: in un unica istanza si sono dunque riunte l’interfaccia vecchia e quella nuova.
Vediamo dunque quali sono i pro e i contro di questo approccio. È utile innanzitutto notare che estendendo l’Adaptee la classe Adapter ha parziale accesso alla sua rappresentazione interna, un vantaggio non da poco quando si considera quanto questo faciliti l’eventuale modifica di funzionalità; inoltre, essa ne eredita le definizioni dei metodi, e se questi non devono cambiare tra la vecchia interfaccia e la nuova si può evitare di ridefinirli totalmente, risparmiando così parecchio codice.
Inoltre, un’istanza della classe Adapter può essere utilizzata attraverso entrambe le interfacce in quanto implementa quella nuova ed eredita quella vecchia; questo aspetto può essere considerato sia un vantaggio che uno svantaggio: se infatti da un lato ciò è molto utile in sistemi che evolvono incrementalmente e in cui dunque alcune componenti potrebbero volersi riferire ancora alla vecchia interfaccia, d’altro canto questo aspetto impedisce di imporre tassativamente che l’oggetto sia utilizzato solo tramite l’interfaccia nuova.
Va poi notato che questo approccio perde un po’ di senso nel caso in cui si debba adattare un’interfaccia e non una classe, in quanto implementare entrambe le interfacce non permette di ereditare codice o funzionalità da quella vecchia. Inoltre, il Class Adapter potrebbe presentare problemi relativi all’ereditarietà multipla, non supportata da alcuni linguaggi a oggetti (es. Java).
Object Adapter
Come abbiamo già detto più volte, spesso conviene prediligere la composizione rispetto all’ereditarietà: al pattern del Class Adapter si contrappone dunque l’Object Adapter, che invece di estendere la classe Adaptee contiene una sua istanza e delega ad essa tramite la vecchia interfaccia le chiamate ai metodi dell’interfaccia nuova, eventualmente operando i necessari rimaneggiamenti.
public class Adapter implements Target {
private final Adaptee adaptee;
public Adapter(Adaptee adaptee) {
assert adaptee != null;
this.adaptee = adaptee;
}
@Override
public void request() {
adaptee.oldRequest();
}
}
Anche in questo caso il client non si accorge di nulla, e in particolare non sarebbe nemmeno in grado di dire con certezza se l’Adapter utilizzato sia un Class Adapter o un Object Adapter: a lui la scelta del paradigma è del tutto trasparente.
Rispetto al Class Adapter l’Object Adapter presenta differenti punti di forza e di debolezza, e il primo di questi ultimi è rappresentato dal fatto che invece di avere un’unica istanza che racchiuda entrambe le interfacce con questo pattern abbiamo invece due istanze (Adapter e Adaptee contenuto), cosa che può costituire un notevole spreco di memoria in certe situazioni.
Inoltre, aver sostituito l’ereditarietà con la composizione ha lo sgradevole effetto di non permettere all’Adapter di vedere in alcun modo la rappresentazione protetta dell’Adaptee, che esso dovrà invece manipolare unicamente tramite la sua interfaccia pubblica. Si è poi costretti a reimplementare ogni metodo anche se questo non è cambiato dall’interfaccia vecchia a quella nuova, in quanto è comunque necessario operare la delega all’Adaptee.
Tuttavia, l’Object Adapter si rivela particolarmente utile nel caso ad essere adattata debba essere un’interfaccia: non soffrendo di problemi di ereditarietà, un Object Adapter ha la peculiarità di poter adattare chiunque implementi la vecchia interfaccia, ovvero un’intera gerarchia di classi potenzialmente non ancora esistenti!
Class Adapter vs Object Adapter
Class Adapter e Object Adapter hanno ciascuno i propri vantaggi e svantaggi che li rendono più adatti ad essere utilizzati in diverse situazioni. Volendo fare un confronto tra i due approcci proponiamo dunque la seguente tabella:
Aspetto | Class Adapter | Object Adapter |
---|---|---|
Accesso all’Adaptee | L’Adapter può accedere ad attribuiti e metodi protetti dell’Adaptee | L’Adapter può interagire con l’Adaptee solo tramite la sua interfaccia pubblica |
Riuso del codice | Non richiede di reimplementare i metodi che non cambiano | Qualunque metodo va reimplementato per fare la delega |
Uso della memoria | Un’unica istanza | Due istanze obbligatorie |
Adozione delle interfacce | L’istanza può essere usata con entrambe le interfacce | L’istanza può essere usata solo tramite la nuova interfaccia |
Problemi di ereditarietà multipla | Possibili | No |
Adattamento delle interfacce | Non è indicato | Adattando un’interfaccia può adattare un’intera gerarchia di classi |
FACADE
Costruendo un sistema complesso può capitare di dover definire una serie di interfacce molto specifiche e dettagliate per i propri componenti in modo che questi possano lavorare correttamente in concerto tra di loro. Il problema sorge però quando un Client, dovendo accedere al sistema, si ritrova costretto a dover interagire direttamente con i sottosistemi che lo compongono, cosa che lo obbliga a sviscerare i funzionamenti interni dello stesso per ottenere un comportamento tutto sommato semplice.
Lo scopo del pattern Facade è allora quello di fornire un’interfaccia unificata e semplificata a un insieme di interfacce separate: spesso infatti l’uso comune di un sistema si riduce un paio di operazioni ottenibili combinando varie funzionalità fornite dal package; invece di richiedere al Client di operare tale composizione facciamo ricadere sulle nostre spalle tale compito costruendo una classe che faccia da interfaccia standard al sistema.
Si noti come questo non impedisca al Client di usare anche le funzionalità più complesse, ma metta solo ulteriormente a disposizione un’interfaccia che gli permetta di sfruttare facilmente quelle più frequentemente utilizzate. Volendo fornire un esempio nella vita reale, un telecomando fornisce un’interfaccia semplice ai controlli della televisione, permettendo di regolare il volume e cambiare canale con semplicità: aprendo però uno sportellino ecco che ci vengono forniti tutti i comandi più specifici.
COMPOSITE
Immaginiamo di dover modellare un file system in un’applicazione: esso sarà composto di File e Directory, le quali dovranno essere in grado di contenere al loro interno File e ulteriori Directory; dovremo cioè ottenere una struttura ad albero di Directory avente dei File come foglie. Se però molte funzionalità del file system operano in modo analogo sia sui File che sulle Directory (es. creazione, cancellazione, ottenimento della dimensione etc.), come possiamo gestire queste due classi in modo uniforme per evitare di duplicare il codice?
Per gestire simili strutture ad albero che rappresentano insiemi e gerarchie di parti viene introdotto il pattern Composite: esso mira a gestire oggetti singoli, gruppi e persino gruppi di gruppi in maniera uniforme e trasparente in modo che un client non interessato alla struttura gerarchica possa utilizzarli senza accorgersi delle differenze.
Abbiamo quindi gli oggetti singoli, rappresentati dalla classe Leaf, e gli oggetti composti rappresentati dalla classe Composite.
Per realizzare l’uniformità di gestione dobbiamo introdurre un livello di astrazione, quindi Leaf e Composite implementano una stessa interfaccia Component contenente la definizione delle operazioni comuni.
L’uso dell’interfaccia comune permette di definire all’interno di Composite le operazioni di aggiunta e rimozione di oggetti al gruppo in modo generale, permettendo cioè che un Composite aggreghi sia Leaf che altri Composite.
A proposito di tale aggregazione, dallo schema UML possiamo notare le relative cardinalità: “0..n” dal lato del Composite e “0..1” da quello del Component. Esse indicano che:
-
Un’istanza di Composite aggrega 0 più istanze di Component al suo interno: in questo modo si permette che al momento della creazione il Composite sia totalmente vuoto; se questo non ha alcun senso logico nell’applicazione si può invece modificare la cardinalità in “1..n” imponendo che al costruttore di Composite venga passato un Component iniziale da contenere;
-
Un’istanza di Component può essere contenuta in al più un’istanza di Composite: può cioè essere libero o aggregato in un gruppo, ma non può appartenere contemporaneamente a più gruppi, cosa che forza una struttura strettamente ad albero.
Nella maggior parte dei casi un’istanza Composite utilizzerà gli oggetti aggregati per implementare effettivamente i metodi descritti dall’interfaccia comune, delegando a loro l’esecuzione effettiva e limitandosi ad elaborare i risultati. Riprendendo l’esempio di prima, per conoscere la dimensione di una Directory sarà sufficiente sommare le dimensioni dei File e delle altre Directory in essa contenuti.
Il patter Composite presenta numerosi vantaggi, ma non è nemmeno esente da criticità.
L’uso di un’interfaccia comune per Leaf e Composite permette al client di non preoccuparsi del tipo dell’oggetto con cui sta interagendo, in quanto ogni Component è in grado di eseguire le operazioni descritte nell’interfaccia in modo indistinguibile; tuttavia, questo implica che non è possibile distinguere tra oggetti singoli e composti.
Inoltre, l’uso dell’interfaccia per l’aggregazione nei Composite rende impossibile imporre dei controlli su cosa possa contenere un certo tipo di Composite: non si può per esempio forzare che raggruppi solo certi tipi di elementi, o che l’albero di composizione abbia profondità al più pari a tre.
Un “dialetto” del pattern tenta di risolvere il problema dell’indistinguibilità tra Leaf e Composite introducendo nell’interfaccia Component un metodo getComposite
che in un Composite restituisca this
e in una Leaf restituisca null
.
L’uso di valori nulli e la necessità di strani casting rende però pericolosa l’adozione di questa versione del pattern.
DECORATOR
Immaginiamo di voler modellare con degli oggetti una grande varietà di pizze differenti sia per la base (es. normale, integrale, senza glutine…) che per gli ingredienti che vi si trovano sopra.
Per ogni diversa varietà di pizza vorremmo ottenere un oggetto aderente a un’interfaccia comune Pizza
il cui metodo toString()
elenchi la base e gli ingredienti che la compongono.
Un primo approccio statico a questo problema consiste nel creare una gerarchia di classi che contenga una classe per ogni possibile combinazione di base e ingredienti, che d’ora in avanti chiameremo “decorazioni”.
public interface Pizza {}
public class BaseNormale implements Pizza {
public String toString() {
return "Sono una pizza con: base normale";
}
}
public class BaseIntegrale implements Pizza {
public String toString() {
return "Sono una pizza con: base integrale";
}
}
public class BaseNormaleSalame extends BaseNormale {
public String toString() {
return "Sono una pizza con: base normale, salame";
}
}
public class BaseNormaleSalamePeperoni extends BaseNormaleSalame {
public String toString() {
return "Sono una pizza con: base normale, salame, peperoni";
}
}
...
Come è subito ovvio, però, questo approccio risulta assolutamente da evitare per una serie di motivi: in primo luogo l’esplosione combinatoria dovuta all’accoppiamento di ogni possibile base e insieme di decorazioni, e in secondo luogo l’estrema difficoltà che comporterebbe una futura aggiunta di decorazioni.
L’ideale sarebbe invece poter aggiungere funzionalità e caratteristiche dinamicamente, restringendo la gerarchia ad un’unica classe le cui istanze possano essere “decorate” su richiesta al momento dell’esecuzione.
La soluzione più semplice a questo nuovo problema parrebbe quella che viene definita una GOD CLASS (o fat class), ovvero un’unica classe in cui tramite attributi booleani e switch
vengono attivate o disattivate diverse decorazioni.
public class GodPizza {
boolean baseNormale = false;
boolean baseIntegrale = false;
...
boolean salame = false;
boolean pancetta = false;
boolean peperoni = false;
...
public void setBaseNormale(boolean status) { baseNormale = status; }
public void setBaseIntegrale(boolean status) { baseIntegrale = status; }
...
public void setSalame(boolean status) { salame = status; }
public void setPancetta(boolean status) { pancetta = status; }
public void setPeperoni(boolean status) { peperoni = status; }
...
public String toString() {
StringBuilder sb = new StringBuilder("Sono una pizza con: ");
if (baseNormale) sb.append("base normale, ");
if (baseIntegrale) sb.append("base integrale, ");
...
if (salame) sb.append("salame, ");
if (pancetta) sb.append("pancetta, ");
if (peperoni) sb.append("peperoni, ");
...
sb.removeCharAt(sb.length() - 1);
sb.removeCharAt(sb.length() - 1);
return sb.toString();
}
}
Si tratta però questo di un chiaro anti-pattern, una soluzione che sebbene invitante e semplice in un primo momento da realizzare nasconde delle criticità non trascurabili. Si tratta infatti di una chiara violazione dell’Open-Close Principle, in quanto per aggiungere un decoratore è necessario modificare la God Class; inoltre, tale classe diventa molto velocemente gigantesca, zeppa di funzionalità tra loro molto diverse (scarsa separazione delle responsabilità) e decisamente infernale da leggere, gestire e debuggare in caso di errori.
Introduciamo dunque il pattern Decorator, la soluzione più universalmente riconosciuta per questo tipo di situazioni.
A prima vista lo schema UML ricorda molto quello del pattern Composite: abbiamo un’interfaccia Component implementata sia da un ConcreteComponent, ovvero una base della pizza nel nostro esempio, sia da una classe astratta Decorator, la quale è poi estesa da una serie di ConcreteDecorator.
A differenza del Composite, tuttavia, qui ciascun Decorator aggrega una e una sola istanza di Component: tali decoratori sono infatti dei “wrapper”, degli oggetti che ricoprono altri per aumentarne dinamicamente le funzionalità.
È importante notare che i Decorator ricevono come oggetto da ricoprire al momento della costruzione un generico Component, in quanto questo permette ai decoratori di decorare oggetti già decorati.
Questo approccio “ricorsivo” permette di creare una catena di decoratori che definisca a runtime in modo semplice e pulito oggetti dotati di moltissime funzionalità aggiunte.
I decoratori esporranno infatti i metodi definiti dall’interfaccia delegando al Component contenuto l’esecuzione del comportamento principale e aggiungendo la propria funzionalità a posteriori: in questo modo la “base” concreta eseguirà il proprio metodo che verrà successivamente arricchito dai decoratori in maniera del tutto trasparente al Client.
public interface Pizza { String toString(); }
public class BaseNormale implements Pizza {
public String toString() {
return "Io sono una pizza con: base normale";
}
}
public class BaseIntegrale implements Pizza {
public String toString() {
return "Io sono una pizza con: base integrale";
}
}
public abstract class IngredienteDecorator implements Pizza {
private Pizza base;
public IngredienteDecorator(Pizza base) { this.base = base; }
public String toString() {
return base.toString();
}
}
public class IngredienteSalame extends IngredienteDecorator {
public IngredienteSalame(Pizza base) { super(base); }
@Override
public String toString() { return super.toString() + ", salame"; }
}
public class IngredientePeperoni extends IngredienteDecorator {
public IngredientePeperoni(Pizza base) { super(base); }
@Override
public String toString() { return super.toString() + ", peperoni"; }
}
public class Client {
public static void Main() {
// Voglio una pizza con salame, peperoni e base integrale
Pizza salamePeperoni =
new IngredientePeperoni(
new IngredienteSalame(
new BaseIntegrale()
)
);
}
}
Vista la somiglianza, inoltre, pattern Decorator e Composite sono facilmente combinabili: si può per esempio immaginare di creare gruppi di oggetti decorati o decorare in un solo colpo gruppi di oggetti semplicemente facendo in modo che Composite, Decorator e classi concrete condividano la stessa interfaccia Component.
Possiamo poi notare una cosa: al momento della costruzione un Decorator salva al proprio interno l’istanza del Component da decorare. Come sappiamo questo darebbe luogo ad un’escaping reference, ma in questo caso il comportamento è assolutamente voluto: dovendo decorare un oggetto è infatti sensato pensare che a quest’ultimo debba essere lasciata la possibilità di cambiare e che debba essere il decoratore ad adattarsi a tale cambiamento.
È interessante poi osservare la classe astratta Decorator: in essa viene infatti inserita tutta la logica di composizione, permettendo così di creare nuovi decoratori con estrema facilità. Spesso, inoltre, se i decoratori condividono una certa parte di funzionalità aggiunte queste vengono anch’esse estratte nella classe astratta creando invece un metodo vuoto protetto che i decoratori reimplementeranno per operare la loro funzionalità aggiuntiva.
public abstract class IngredienteDecorator implements Pizza {
private Pizza base;
public IngredienteDecorator(Pizza base) { this.base = base; }
public String toString() {
return base.toString() + nomeIngrediente();
}
protected String nomeIngrediente() { return ""; }
}
public class IngredienteSalame extends IngredienteDecorator {
public IngredienteSalame(Pizza base) {super(base);}
@Override
public String nomeIngrediente() { return ", salame"; }
}
public class IngredientePeperoni extends IngredienteDecorator {
public IngredientePeperoni(Pizza base) {super(base);}
@Override
public String nomeIngrediente() { return ", peperoni"; }
}
Si noti come l’uso della visibilità protected
renda l’override del metodo possibile anche al di fuori del package, aumentando così la facilità di aggiunta di nuovi decoratori.
Volendo vedere un esempio concreto di utilizzo di questo pattern è sufficiente guardare alla libreria standard di Java: in essa infatti gli InputStream
sono realizzati seguendo tale schema.
STATE
Come sappiamo, le macchine a stati finiti sono uno dei fondamenti teorici dell’informatica: si tratta di oggetti matematici che modellano sistemi in grado di evolvere, ovvero il cui comportamento varia in base allo stato in cui si trovano.
Volendo rappresentare un oggetto di questo tipo la prima idea potrebbe essere quella di realizzare il cambio di comportamento con una serie di if
e switch
, un approccio che come abbiamo già visto numerose volte diventa presto difficilmente sostenibile.
In alternativa ad esso si introduce invece lo State pattern che mantenendo l’astrazione delle macchine a stati finiti permette di modellare facilmente il cambiamento di comportamento di un oggetto al modificarsi dello stato.
Si noti che rimanendo legato al concetto di automa a stati finiti uno dei punti di forza di questo pattern è la semplicità di apportare delle modifiche al codice quando le specifiche di ciò che è stato modellato tramite una macchina a stati finiti cambiano.
Un esempio di utilizzo di questo pattern potrebbe essere un software di editing di foto, in cui l’utente ha a disposizione una toolbar con diversi strumenti che gli permettono di compiere operazioni diverse sullo stesso piano di lavoro (comportamenti diversi dell’azione “tasto sinistro sullo schermo” in base al tool selezionato).
In un automa a stati finiti le componenti fondamentali sono tre:
- gli stati, tra cui si distingue lo stato corrente;
- le azioni che si possono intraprendere in qualunque stato;
- le transizioni da uno stato all’altro come effetto ulteriore di un’azione (es. vim che con ‘i’ entra in modalità inserimento se era in modalità controllo).
Come si vede dallo schema UML, il pattern State cerca di modellare ciascuna di queste componenti: un’interfaccia State raggruppa la definizione di tutte le azioni, rappresentate da metodi, mentre una classe concreta per ogni stato definisce che effetto hanno tali azioni quando ci si trova al suo interno con l’implementazione dei suddetti metodi.
Infine, una classe Context contiene un riferimento ad uno stato che rappresenta lo stato corrente e delega ad esso la risposta alle azioni (che possono essere viste come degli “eventi”); essa espone inoltre un metodo setState(State)
che permette di modificare lo stato corrente.
public class Context {
private State state;
public void setState(@NotNull State s) {
state = s;
}
public void sampleOperation() {
state.sampleOperation(this)
}
}
Rimane dunque solo da definire come si realizzano le transizioni di stato: chi ha la responsabilità di cambiare lo stato corrente? Esistono due diversi approcci, ciascuno dei quali presenta delle criticità:
-
gli State realizzano le transizioni: volendo rimanere aderenti al modello degli automi a stati finiti, possiamo permettere che gli stati concreti chiamino il metodo
setState
del Context all’interno della loro implementazione dei metodi se come effetto di un’azione lo stato corrente cambia. Tuttavia, poichésetState
chiede in input lo stato a cui transizionare questo approccio richiede che gli stati si conoscano tra di loro: si introduce così una dipendenza tra stati non chiaramente visibile nello schema UML e si ha uno sparpagliamento della conoscenza sulle transizioni che rende questo metodo un po’ “sporco”. -
il Context realizza le transizioni: con questa seconda strategia è compito del contesto eseguire le transizioni di stato, evitando così che gli stati si debbano conoscere; l’unico depositaria della conoscenza sulle transizioni è la classe Context. Ciascuna azione viene dunque intrapresa in due step: il Context richiama il corrispondente metodo dello stato corrente e successivamente ne intercetta il risultato; può dunque decidere tramite esso se cambiare stato e eventualmente a quale stato transizionare.
Si tratta tuttavia di un ritorno al table-driven design fatto diif
eswitch
da cui ci eravamo voluti allontanare: come in quel caso, l’approccio risulta fattibile soltanto finché ci sono poche possibili transizioni. Inoltre, se una transizione non dipende dal risultato di un’azione ma da come questa è stata eseguita questo approccio è totalmente impossibile in quanto tale tipo di conoscenza non è presente nella classe Context.
Per via delle difficoltà poste dal secondo approccio si sceglie spesso di effettuare le transizioni all’interno degli stati: questo permette di rendere esplicito e atomico il passaggio di stato.
A tal proposito, è interessante notare come le istanze degli stati concreti non posseggano alcuna informazione di stato in quanto il Context a cui si riferiscono viene passato loro al momento della chiamata dei rispettivi metodi: al di là della loro identità essi sono completamente stateless.
Si tratta di un approccio molto utile in caso si debbano modellare più macchine a stati finiti dello stesso tipo, in quanto l’assenza di stato rende le stesse istanze degli stati concreti condivisibili tra diversi Context, in una sorta di pattern Singleton.
Volendo trovare ulteriori analogie con altri pattern, il pattern State ricorda nello schema il pattern Strategy: la differenza sta però nel fatto che i diversi stati concreti sono a conoscenza l’uno dell’altro, mentre le strategie erano tra di loro completamente indipendenti.
FACTORY METHOD
Talvolta capita che un certo Client sia interessato a creare un oggetto non in base al suo tipo quanto all’interfaccia che esso implementa: ad esso non importa conoscere la classe di cui l’oggetto è un’istanza perché essa non ha alcuna rilevanza nel suo contesto.
Tuttavia, la normale creazione di un oggetto tramite la keyword new
richiede di esplicitare la classe a cui esso appartiene, costringendo così il Client ad approfondire inutilmente la sua conoscenza sui tipi che implementano l’interfaccia a cui è interessato.
Per evitare questo tipo di situazione introduciamo uno dei cosiddetti pattern creazionali, ovvero legati alla creazione di oggetti: stiamo parlando del pattern dei Factory methods. Esso definisce una classe astratta Creator dotata di metodi fabbrica astratti che restituiscono un’istanza di un tipo aderente all’interfaccia Product a cui il Client è interessato: a quale classe appartenga effettivamente tale istanza (Product concreto) è però lasciato ad un Creator concreto tra i tanti che estendono la classe astratta; idealmente dovrebbe esserci un creatore concreto per ogni tipo di prodotto concreto che implementa l’interfaccia Product.
Questo pattern definisce dunque un’interfaccia per creare un Product ma lascia al Creator concreto la scelta di cosa creare effettivamente: in questo modo all’interno della classe astratta Creator è possibile scrivere dei metodi che richiedono la creazione di un Product pur senza sapere di preciso il tipo dell’oggetto che verrà creato, in quanto questo sarà determinato dall’implementazione di factoryMethod
del creatore concreto.
Si sfruttano dunque al massimo grado polimorfismo e collegamento dinamico, in quanto il tipo dell’oggetto da creare viene deciso a runtime: poiché nemmeno il Creator conosce il tipo concreto dei Product creati risulta dunque subito chiaro perché i factory methods non possano essere metodi statici di tale classe.
I factory methods rappresentano un esempio dell’utilità delle astrazioni permesse dai linguaggi ad oggetti: in un contesto in cui normalmente non è possibile fare overriding, come un costruttore, la soluzione è quella di virtualizzare il tutto con la creazione di metodi che possono essere esportati in classi concrete.
Per questo motivo i factory method vengono talvolta detti anche virtual constructors, “costruttori virtuali”.
Per capire meglio il funzionamento del pattern, vediamo un esempio di come esso può essere utilizzato.
Consideriamo un software capace di aprire contemporaneamente più documenti di tipo differente in diverse pagine, come per esempio Microsoft Word o Excel: al loro interno, quando viene creato un nuovo file vengono fatte una serie di operazioni generiche (creare la nuova pagina, mostrare vari popup…), ma ad un certo punto è necessario creare un oggetto che rappresenti il nuovo documento e il cui tipo dipende dunque dal documento creato.
Il codice di creazione del nuovo oggetto Documento
non può dunque trovarsi in un metodo della classe astratta Application
(Creator) insieme con il resto delle operazioni generiche in quanto specifico della tipologia di documento creato: è dunque necessario virtualizzare la creazione dell’oggetto in un metodo createDocument()
implementato da una serie di sottoclassi concrete MyApplication
(ConcreteCreator) ciascuna specifica per un tipo di documento.
ABSTRACT FACTORY
Vediamo ora una generalizzazione del Factory method pattern che si utilizza quando, al posto di creare un solo oggetto aderente ad un’interfaccia, è necessario creare più oggetti aderenti a varie interfacce i cui tipi concreti siano però compatibili tra di loro.
Immaginiamo per esempio di aver progettato un’applicazione cross-platform e di doverne creare la User Interface: essa dovrà avere stili diversi in base al sistema operativo sui cui si sta eseguendo. Non conoscendo su quale os si starà operando, il resto dell’applicazione gestirà gli elementi dell’UI tramite delle opportune interfacce che nascondano il tipo concreto delle istanze, il quale determinerà lo stile con cui esse verranno rappresentate: sarà però fondamentale che tutti gli elementi dell’UI condividano lo stesso stile in modo da non creare un’orrendo arlecchino.
Ecco dunque che introduciamo il pattern delle Abstract Factory, un metodo in grado di fornire un’interfaccia per creare famiglie di oggetti compatibili tra loro senza specificare la loro classe concreta così da garantire una certa omogeneità all’insieme.
Per fare ciò il pattern propone di creare un’interfaccia AbstractFactory contenente la definizione di un factory method per ogni tipo di prodotto astratto (Product) e una serie di ConcreteFactory che restituiranno dei ConcreteProduct in uno specifico stile: in questo modo, interagendo con una Factory concreta un Client potrà ottenere in modo a lui trasparente una serie di prodotti concreti coerenti in stile tra di loro.
Tornando al problema della User Interface, volendo sfruttare l’Abstract Factory pattern dobbiamo creare un’interfaccia GUIFactory
che contenga la dichiarazione di due metodi creazionali, createButton()
e createCheckbox()
: questi permetteranno al client di creare un bottone e una checkbox nello stile specificato dalla classe concreta della factory; per ciascuno di tali elementi dell’UI dobbiamo dunque creare un’interfaccia prodotto, ovvero rispettivamente le interfacce Button
e Checkbox
.
All’interno delle classi factory concrete tali metodi creazionali restituiranno però dei prodotti concreti nello stile specifico della factory da cui sono prodotti: così, per esempio, una MacFactory
(per lo stile di MacOs) creerà MacButton
e MacCheckbox
, mentre una WinFactory
(per lo stile di Windows) creerà WindowsButton
e WinCheckbox
.
In questo modo la nostra applicazione dovrà possedere al suo interno unicamente un riferimento alla factory adatta al sistema operativo su cui sta girando e potrà creare tramite essa tutti gli elementi di UI di cui avrà bisogno senza preoccuparsi di specificare ogni volta lo stile: la factory concreta glielo restituirà sempre nello stile selezionato inizialmente.
MODEL VIEW CONTROLLER
Spesso nelle applicazioni capita che uno stesso dato sia riportato tramite diverse viste all’interno dell’interfaccia utente: il colore di un testo, per esempio, potrebbe essere rappresentato contemporaneamente da una terna di valori RGB, dal suo valore esadecimale e da uno slider di colori.
Si tratta di modi differenti di rappresentare la medesima informazione condivisa, che viene replicata più volte per dare all’utente diversi modi in cui visualizzarla.
La condivisione di un medesimo valore porta però con sé un problema: se tale dato viene modificato dall’utente interagendo con una delle viste è necessario che tale modifica venga propagata a tutte le altre viste in modo da mantenere l’informazione coerente.
Abbiamo dunque bisogno di un framework che ci permetta di mantenere un’informazione condivisa in modo efficiente e pulito e che permetta di rappresentarla facilmente sotto diversi punti di vista: l’invitante soluzione di fare semplicemente sì che le viste comunichino direttamente i cambiamenti del dato l’una con l’altra si rivela infatti velocemente impraticabile. Il pattern Model View Controller (MVC) propone invece di suddividere la gestione del dato e dell’interazione con l’utente in tre tipologie di classi:
- Model: un’unica classe contenente lo stato condiviso; si tratta dell’unico depositario dell’informazione con cui tutte le viste dovranno comunicare per aggiornare i dati mostrati.
- View: una serie di classi che costituiscono l’interfaccia con l’utente; esse mostrano il dato secondo il loro specifico punto di vista e permettono all’utente di interagire con l’applicazione.
- Controller: ciascuna vista possiede infine una classe di controllo collegata che si occupa della logica dell’applicazione; ogni volta che l’utente interagisce con una vista tale interazione viene passata al relativo Controller, che si occuperà di rispondere all’input eventualmente modificando lo stato condiviso nel Model.
Abbiamo dunque una suddivisione dell’applicazione in tre tipi di componenti differenti che cooperano tra di loro senza però essere strettamente dipendenti l’uno dall’altro. Un tipico ciclo di interazione tra le tre componenti funziona infatti come mostrato in figura:
- Una View riceve un’interazione da parte dell’utente e comunica tale evento al proprio Controller;
- Il Controller gestisce l’interazione e se essa richiede un cambiamento dello stato comune chiede al Model di modificare il proprio contenuto;
- Come ulteriore passaggio, il Controller aggiorna il dato mostrato dalla View ad esso associata prima ancora che il modello sia cambiato;
- Ricevuta la richiesta, il Model aggiorna l’informazione condivisa e notifica tutte le View del cambiamento: in questo modo esso non avrà effetto solo nella vista che ha ricevuto l’input dell’utente ma in tutte;
- Le View ricevono la comunicazione del fatto che il Model è cambiato e aggiornano la propria informazione mostrata recuperando il dato aggiornato dal modello (politica pull).
Questo modello di interazione circolare permette di separare l’interfaccia utente (view) dall’interfaccia dello stato comune (model) e dalla logica del cambiamento di stato (controller): grazie alla mediazione del Controller le View non hanno bisogno di conoscere direttamente la struttura dei dati contenuti nel Model, cosa che ci permette di riutilizzare le stesse View, e dunque le stesse interfacce utente, per dati diversi (es. una casella di testo è una View e non dipende dal dato che ci si inserisce).
È inoltre interessante notare come un Controller potrebbe voler comunicare dei cambiamenti virtuali alla View da cui è partito un input prima ancora che al Model venga chiesta un eventuale modifica dello stato.
Nel caso ci siano errori nell’input inserito dall’utente, infatti, esso va informato in qualche modo: il Controller non cambierà dunque lo stato condiviso ma solo lo stato dalla relativa View in modo da mostrare un qualche messaggio d’errore.
Similmente, se i dati inseriti sono già presenti nel Model (cosa che il Controller non può sapere a priori) quest’ultimo potrebbe avvisare il Controller di tale evenienza al momento della richiesta di cambiamento: esso dovrà dunque nuovamente notificare l’utente che l’inserimento dei dati non è andato a buon fine aggiornando la propria View.
Portiamo ora attenzione su un altro aspetto: nell’insieme dei meccanismi che realizzano il pattern Model View Controller si possono riscontrare una serie di altri pattern che abbiamo già trattato. Per agevolare la comprensione del funzionamento di questo nuovo “mega-pattern”, vediamo quindi quali sono i pattern utilizzati al suo interno:
-
Observer, poiché le View sono Observer del Model: ogni vista si registra come Observer del modello in modo che il Model, in pieno stile Observable, le notifichi dei suoi cambiamenti di stato.
Spesso la strategia di aggiornamento delle viste è qui quella pull, ovvero quella secondo cui agli Observer viene passato un riferimento all’oggetto Observable in modo che siano loro stessi a recuperare i dati di cui hanno bisogno tramite opportuni metodi getter: questo permette infatti di memorizzare nello stesso Model i dati di diverse View.
Va inoltre fatto notare che se l’interfaccia esposta dalle View è un’interfaccia a eventi, come per esempio un’interfaccia grafica (es. un click sullo schermo genera un evento), anche la comunicazione tra View e Controller può avvenire tramite il pattern Observer: ciascun Controller si registra infatti come Observer degli eventi che avvengono sulla View. - Strategy, poiché i Controller sono Strategy per le View: poiché ad ogni vista è collegato uno e un solo Controller che regola come la vista reagisca agli input dell’utente, i Controller possono essere visti come strategie di gestione degli eventi generati dalle viste. Poiché le viste sono componenti sostanzialmente “stupidi” che risolvono le interazioni dell’utente delegando al proprio Controller la loro gestione, questo approccio permette per esempio di gestire viste identiche in modi diversi semplicemente cambiando il Controller ad esse associato: così, per esempio, è possibile rendere una casella di testo read-only oppure modificabile senza modificare in alcun modo la classe della relativa vista e rispettando così l’Open-Close Principle.
- Composite, poiché le View sono spesso composte da più Component: quando le View rappresentano interfacce grafiche (GUI) esse sono spesso realizzate componendo diversi elementi tra di loro (es. aree di testo, bottoni, etc…). Per questo motivo è spesso prevalente il pattern Composite nella loro implementazione, utile specialmente per quanto riguarda la creazione su schermo dell’interfaccia, che viene disegnata pezzo per pezzo.
In conclusione, il Model è in grado di interagire con tutte le viste che l’osservano tramite un unico comando (update), mentre le View comunicano con il Model passando attraverso il Controller, che fa da una sorta di “Adapter” tra i due. Questo permette allo stesso dato di avere interfacce disomogenee senza alcun tipo di problema riguardante la coerenza dello stesso.
Tuttavia, il problema principale del pattern Model View Controller è la dipendenza circolare tra le tre componenti: le view comunicano ai rispettivi controller gli eventi, questi li elaborano e aggiornano il modello il quale a sua volta avvisa le view dei cambiamenti di stato.
Questa struttura fortemente interconnessa rende difficoltoso lo sviluppo e il testing in quanto non esiste un chiaro punto da cui partire a costruire: si potrebbe pensare di fare mocking delle view e iniziare a sviluppare il resto, ma questo approccio porta comunque a una serie di inutili complicazioni; bisogna inoltre considerare che il testing delle view è spesso particolarmente complesso coinvolgendo varie funzioni di libreria o funzioni grafiche.
Come vedremo nel prossimo paragrafo, per ovviare a questo problema si decide spesso di spezzare il circolo vizioso di Model, View e Controller modificando lievemente le rispettive dipendenze.
MODEL VIEW PRESENTER
Come preannunciato esiste una variante del Model View Controller chiamata Model View Presenter che fornisce una soluzione al problema del testing delle viste e delle relative interfacce grafiche. Questo nuovo pattern eleva il ruolo del Controller, ora chiamato Presenter, a completo intermediario tra View e Model in entrambi i sensi di comunicazione: non solo dunque le View delegano ai rispettivi Presenter la gestione delle interazioni con l’utente, ma al momento del cambiamento dell’informazione condivisa il Model notifica non direttamente le viste ma i Presenter stessi, i quali avranno dunque il compito di aggiornare la propria View per mostrare il dato modificato.
Model e View perdono dunque alcun legame diretto, facendo apparire sempre più i Presenter come Adapter tra stato concreto (model) e stato virtuale mostrato all’utente (view). La rottura di tale legame facilita il testing delle viste poiché invece di verificare che una vista e la rispettiva controparte grafica abbiano ricevuto e processato correttamente un aggiornamento del dato da parte del Model è sufficiente verificare che un update del Model provochi nei Presenter un aggiornamento del dato mostrato dalla propria View: siamo dunque riusciti a isolare l’interfaccia logica da quella grafica, rendendo più semplice il testing di entrambe e sfoggiando un esempio importante del cosiddetto design for testing.
In ultimo, utilizzando questo pattern è importante fare attenzione di mantenere segreta la rappresentazione interna del Model ai Presenter e viceversa, evitando in particolar modo eventuali escaping reference: la separazione delle responsabilità costruita con la suddivisione dei dati dalla loro logica di gestione perderebbe infatti alcuna valenza se si legassero troppo strettamente Model e Presenter.
BUILDER
Può talvolta capitare che l’inizializzazione di un’istanza di una classe richieda un numero molto grande di parametri, alcuni dei quali obbligatori e altri facoltativi. Come si realizzano i costruttori della classe in questo tipo di situazioni?
Telescoping constructor pattern
L’approccio più immediato a questo problema è quello dei costruttori telescopici (telescoping constructor pattern): all’interno della classe si realizza un costruttore completo che richiede tutti i parametri e una serie di costruttori secondari che invece prendono i parametri obbligatori e diverse combinazioni dei parametri opzionali, rimappando poi spesso la propria esecuzione sul costruttore completo tramite l’assegnamento di valori di default ai parametri non ricevuti.
public class MyClass {
private final T0 optionalField1;
private final T1 mandatoryField;
private final T2 optionalField2;
public MyClass(T1 mf) {
this(defaultValue1, mf, defaultValue2);
}
public MyClass(T1 mf, T0 of) {
this(of, mf, defaultValue2);
}
public MyClass(T1 mf, T2 of) {
this(defaultValue1, mf, of);
}
public MyClass(T1 mf, T0 of1, T2 of2) {
this.optionalField1 = of1;
this.optionalField2 = of2;
this.mandatoryField = mf;
}
}
Questa tecnica si rivela però presto molto poco funzionale: innanzitutto il numero di costruttori da realizzare cresce esponenzialmente nel numero di parametri opzionali, rendendo la classe estremamente confusionaria.
Sorgono inoltre dei problemi nel caso di parametri opzionali dello stesso tipo, in quanto è impossibile disambiguare tra di essi al momento della definizione dei costruttori: con due parametri opzionali dello stesso tipo, per esempio, non sarebbe possibile distinguere il costruttore che assegni il primo ma non il secondo e viceversa (si noti come non si può nemmeno distinguere tramite il nome del costruttore in quanto questo deve necessariamente essere lo stesso della classe).
Se linguaggi come Python risolvono questo problema imponendo che il chiamante di un costruttore espliciti il nome del parametro opzionale che sta assegnando, questo tipo di meccanismo non esiste in Java: ciò ci costringerebbe quindi a far sì che nei costruttori vengano passati o tutti i parametri dello stesso tipo o nessuno di essi.
JavaBeans pattern
Per risolvere i problemi appena visti la prossima soluzione che viene in mente è dunque quella di fornire un unico costruttore che prenda in input solamente i parametri obbligatori e creare poi una serie di setter per i parametri opzionali: si tratta del cosiddetto pattern JavaBeans.
public class MyClass {
private T0 optionalField1;
private T1 mandatoryField;
private T2 optionalField2;
public MyClass(T1 mf) {
this.mandatoryField = mf;
}
public void setOptionalField1(T0 of) {
this.optionalField1 = of;
}
public void setOptionalField2(T2 of) {
this.optionalField2 = of;
}
}
Anche questo approccio presenta tuttavia delle notevoli difficoltà.
In primo luogo, un oggetto costruito con il pattern JavaBeans non può essere immutabile in quanto richiede la presenza di setter per i propri attributi opzionali (che dunque non possono essere final
): possiamo dunque creare solo oggetti mutabili.
Un problema forse più grave è inoltre che questo pattern ammette la presenza di momenti nella vita di un oggetto in cui esso non è stato ancora costruito completamente: tra la creazione e l’assegnamento tramite setter dei parametri opzionali, infatti, l’istanza si trova in uno stato non finito e dunque non consistente che potrebbe creare numerosi problemi in sistemi di tipo concorrente o multi-thread.
Builder pattern
Gli autori del libro Effective Java propongono dunque un nuovo pattern che prende gli aspetti migliori della prima e della seconda soluzione finora proposta risolvendo al tempo stesso i problemi di entrambe: essa permetterà infatti di creare oggetti immutabili (rendendo gli attributi final
) e di assegnare solo alcuni dei parametri opzionali senza generare problemi di inconsistenza o di sovrapposizione dei tipi degli attributi.
Questo pattern creazionale prende il nome di Builder.
Data una classe da costruire MyClass
avente parametri obbligatori e opzionali il primo passo è quello di rendere privato il suo costruttore, il quale prenderà in input non più una lista di parametri ma un’istanza di una nuova classe Builder
.
Tale classe viene definita come una classe statica, pubblica e interna a MyClass
, con la quale condivide il tipo e il numero di attributi obbligatori e opzionali (questi ultimi subito inizializzati al loro valore di default).
Seguendo il pattern JavaBeans, la classe Builder esporrà un costruttore pubblico contenente solo i parametri obbligatori e una serie di setter per i parametri opzionali.
Ma a che pro costruire un oggetto della classe Builder quando quella che volevamo ottenere era un’istanza di MyClass
?
La risposta sta nella definizione metodo build()
: tramite esso, il Builder restituirà un’istanza di MyClass inizializzata con propri i parametri obbligatori e opzionali; essendo una classe interna, infatti, il Builder sarà l’unico in grado di accedere al costruttore privato di MyClass
.
public class MyClass {
private final T0 optionalField1;
private final T1 mandatoryField;
private final T2 optionalField2;
private MyClass(Builder builder) {
mandatoryField = builder.mandatoryField;
optionalField1 = builder.optionalField1;
optionalField2 = builder.optionalField2;
}
public static class Builder {
private T1 mandatoryField;
private T0 optionalField1 = defaultValue1;
private T2 optionalField2 = defaultValue2;
public Builder(T1 mf) {
mandatoryField = mf;
}
public Builder withOptionalField1(T0 of) {
optionalField1 = of;
return this;
}
public Builder withOptionalField2(T2 of) {
optionalField2 = of;
return this;
}
public MyClass build() {
return new MyClass(this);
}
}
}
Questo pattern è particolarmente intelligente per una serie di motivi: innanzitutto, rendendo privato il costruttore di MyClass
ci si assicura che le sue istanze siano costruite unicamente tramite il Builder.
A tal proposito, il fatto che Builder
sia una classe statica è di non poca importanza: questo permette infatti di creare una sua istanza senza prima istanziare la classe che la contiene, cosa che come abbiamo visto sarebbe impossibile essendo il costruttore di MyClass privato.
Per creare un’istanza di Builder è dunque sufficiente la seguente sintassi:
MyClass.Builder = new MyClass.Builder(...);
Si potrebbe notare che essendo statica la classe Builder potrà accedere solamente agli elementi statici di MyClass
, ma questo non costituisce un problema: come abbiamo visto, essa dovrà solamente richiamarne il costruttore, che per sua stessa natura è sempre statico.
È importante notare che non vale però il contrario: MyClass
, una volta ricevuta un’istanza di Builder come parametro del costruttore, può benissimo accedere ai suoi campi privati e sfrutta questa possibilità per copiare i valori dei parametri obbligatori e opzionali che il Builder ha ricevuto all’interno dei propri attributi.
Assegnando tali valori al momento della creazione, gli attributi di MyClass
potranno quindi anche essere final
, permettendo così la creazione di oggetti immutabili.
Un altro particolare da sottolineare è che i setter degli attributi opzionali del Builder sono setter un po’ “spuri”, in quanto invece di non ritornare nulla ritornano il Builder stesso: questo permette infatti di concatenare più setter l’uno con l’altro ottenendo così una notazione più fluente.
È possibile infatti creare inline un’istanza di Builder, settare direttamente i suoi parametri opzionali e infine richiamare il metodo build()
per ottenere facilmente un’istanza di MyClass
:
MyClass inst = (new MyClass.Builder(mandatoryField).withOptionalField1(optionalField1)).build();
L’utilizzo di un Builder risolve inoltre eventuali problemi dovuti alla concorrenza: quando viene chiamato il metodo build()
l’istanza di MyClass
viene restituita già completa, ovvero con tutti i parametri obbligatori e opzionali al valore desiderato (o di default se nessun setter è stato chiamato).
Abbiamo così eliminato la possibilità di inconsistenze e creazioni parziali delle istanze di MyClass
.