Ne esamineremo i principali riassumendone le caratteristiche ed il funzionamento.
In questo caso la richiesta è eseguita sempre solo da uno dei nodi della gerarchia, ma non è escluso che un nodo possa effettuare delle operazioni e poi girare comunque la richiesta al parent node.
Se nessun nodo gestisce la richiesta, il flusso dell'invocazione risalirà sino al nodo root della gerarchia, ad esempio se la gerarchia è data dai controlli visuali presenti un una maschera, il root sarà la maschera stessa. La root potrebbe ignorare la richiesta o implementare un comportamento generico.
In questo schema c'è un accoppiamento abbastanza ridotto, poiché ogni nodo conserva al massimo un riferimento al proprio parent, c'è il vantaggio che non è necessario conoscere in partenza quale nodo eseguirà effettivamente la funzione richiesta, e però non si ha neanche la certezza che qualcuno la gestirà. Eventualmente la root in questo caso potrebbe visualizzare un messaggio o intraprendere azioni più generiche. In questo caso la classe potrebbe assomigliare a:
class Handler:IHandler {
IHandler parentNode;
public Handler(IHandler parent){
this.parentNode=parent;
}
public void HandleRequest(object input){
if (può gestire la richiesta rappresentata dall'input) {
//codice che gestisce la richiesta
}
else {
if(parentNode != null) { //se c'è un nodo parent
parentNode.HandleRequest(input); //gira la richiesta al parent
}
else {
//Messaggio di errore: comando non eseguito
}
}
}
}
Dato che i comandi possono essere diversi, si pone il problema di come passare l'input ai vari comandi. Un metodo può essere avere un oggetto di tipo IRequest:
interface IRequest {
CommandKind getKind();
object Data;
}
e poi nel metodo HandlerRequest(IRequest data) esaminare il kind del parametro per stabilire il tipo effettivo di dati presenti nel campo Irequest.Data, ad esempio supponiamo di voler processare con un certo nodo solo le richieste di "tipo 1":
public void HandleRequest( IRequest data ){
if (data.getKind()=="tipo 1") {
//codice che gestisce la richiesta
TDato1 d1 = (TDato1) data.Data;
//codice che opera su d1...
return;
}
if(parentNode != null) { //se c'è un nodo parent
parentNode.HandleRequest(data); //gira la richiesta al parent
}
}
Command
Rappresenta una richiesta di esecuzione di una funzione in un oggetto in modo che possa essere eseguito in una coda o possa esserne effettuato il rollback.
L'informazione memorizzata include sia i parametri di invocazione, che il riferimento dell'oggetto su cui deve essere eseguita, e anche il nome (o qualsivoglia discrimine) della funzione da eseguire.
Se si vuole poter effettuare il rollback, potrebbe essere necessario anche memorizzare tutto o parte dello stato dell'oggetto da invocare al momento dell'esecuzione della funzione.
Una volta che si è incapsulato in un oggetto tutti i dati necessari ad invocare una funzione, si ha il vantaggio di poterlo trattare come un qualsiasi dato e quindi per esempio:
- creare macrocomandi, ossia elenchi di comandi da eseguire in sequenza
- consentire di salvare un log delle azioni effettuate
- consentire l'"undo" di una o più operazioni ove nell'oggetto vi siano anche le informazioni sull'oggetto al momento dell'applicazione della funzione
- consentire il "redo" ossia applicare di nuovo una operazione
- come conseguenza della precedente, consentire le "transazioni", ossia invertire le prime n azioni di una sequenza ove la (n+1) generi un errore, in sostanza consentire di eseguire "tutto il blocco o niente"
In questo pattern distinguiamo:
- un'interfaccia ICommand implementata da tutti i comandi
- delle classi Comando_xxx che implementano l'interfaccia ICommand
- un client che crea i Comandi Comando_xxx e stabilisce su quale oggetto applicarli
- l'invocatore che richiede al comando di agire
- il Receiver, tipicamente un documento o un'applicazione, su cui applicare i comandi
Possiamo osservare che il Command disaccoppia completamente l'oggetto che richiede l'operazione dall'oggetto su cui è effettivamente applicata, infatti l'invocatore richiama un metodo di ICommand e la classe che ne implementa l'interfaccia eseguirà un metodo sul receiver in base ai parametri impostati. Questo potrebbe anche diventare utile ai fini di cambiare l'interfaccia di una classe receiver, o usare un'altra classe concreta Receiver senza dover necessariamente cambiare il client.
Interprete
Data una espressione scritta in un certo
linguaggio, ne effettua la valutazione seguendo la
grammatica associata al linguaggio stesso.
La grammatica esprime quali sono le unità elementari del linguaggio e come esse si compongono per creare sequenze più complesse. Una semplice grammatica è quella delle espressioni algebriche, ma come noi informatici sappiamo, anche tutti i linguaggi di programmazione possiedono una loro grammatica, senza parlare della grammatica dei linguaggi naturali.
L'interprete associa ad ogni unità elementare un'azione, e ad ogni regola di grammatica altrettante azioni, in modo che da una certa espressione o frase appartenente al linguaggio, questa venga decomposta in una gerarchia di oggetti ad ognuno dei quali possa essere applicata la relativa azione in modo ricorsivo, a partire dalla regola di più alto livello.
Personalmente, mi sono sempre divertito molto ad usare questo pattern, sia quando in gioventù ho scritto un compilatore lisp ed uno pascal, e sia in seguito quando ho scritto un traduttore da filtri in TSQL a c# per poter creare delle query da applicare sulle strutture in memoria, e viceversa trasformare un oggetto "query" c# in stringa per poterlo eseguire su db, usando dialetti diversi a secondo di un driver generico per un db. Non è un pattern di uso frequente, ma ci sono casi in cui è assolutamente l'unica possibilità per risolvere determinati problemi.
Probabilmente la parte più complicata è quella di scrivere il parser che decompone le frasi del linguaggio nella gerarchia di oggetti, ma una volta che si fa la pratica diventa facile, senza considerare che ci sono librerie in circolazione che agevolano questa fase, e che tuttavia non ho mai usato.
In questo pattern distinguiamo:
- un interprete, che dato un albero derivante da un'espressione è in grado di valutarla
- espressioni elementari (semplici caratteri o segni di punteggiatura, o sequenze prestabilite)
- espressioni non elementari date dalla concatenazione delle precedenti secondo determinati criteri
- un contesto da applicare nella valutazione dell'espressione (ad esempio i valori da attribuire a delle variabili)
- un client che costruisce l'albero sintattico a partire da una sua sequenza e chiede all'interprete la sua valutazione
Una volta decomposto lo statement in un albero sintattico, la valutazione del risultato sarà la composizione di una serie di oggetti ognuno associato ad un elemento (elementare o composto) della frase stessa, e tipicamente con una classe diversa per ogni elemento delle regole della grammatica considerata. Ad esempio ci potrà essere una classe che effettua l'operazione somma, una l'operazione and logico etc, e ogni oggetto-operazione (elemento composto) conserverà i riferimenti agli oggetti relativi agli operandi (o elementi più elementari) e cosi via.
Strategy pattern
Definisce una famiglia di algoritmi, li incapsula in un insieme di classi, e li rende intercambiabili. Lo strategy pattern rende il client indipendente dall'algoritmo utilizzato.
E' possibile utilizzare più strategy pattern in composizione per creare una logica applicativa dinamica e configurabile.
Usare lo strategy pattern aiuta a non creare tante classi simili e che differiscono solo per un comportamento. E' vero che creando una gerarchia di classi si potrebbe ottenere un effetto simile, ma non sarebbe possibile cambiare in itinere il comportamento, e soprattutto, ove siano presenti più punti di variazione, costringerebbe a creare tante classi derivate quante il prodotto cartesiano di tutte le varianti.
Si potrebbe anche inserire nel codice dei vari metodi delle selezioni per usare un algoritmo (o una logica in generale) o un altro ma l'intersecarsi dei vari algoritmi renderebbe il codice poco leggibile e manutenibile. Separare invece le diverse logiche in oggetti distinti semplifica il codice e anche il suo unit testing.
In questo pattern distinguiamo:
- IStrategy, l'interfaccia comune alle classi che implementano i diversi algoritmi
- "ConcreteStrategy" le varie classi che implementano i diversi algoritmi
- Context, il client in cui è presente un riferimento alla classe specifica che implementa l'interfaccia IStrategy e può eventualmente definire un'interfaccia che consenta a tale classe di accedere ai propri dati in qualche modo.
Fondamentale nel progettare uno strategy pattern è quindi definire un'interfaccia comune per tutte le ConcreteStrategy, che sia abbastanza generica da poter consentire ad ognuna di operare ed esporre la stessa interfaccia, d'altronde non è necessario che tutti i parametri sia effettivamente significativi. Inoltre è bene che IStrategy sia più semplice possibile per non renderne l'uso eccessivamente complicato. E' possibile anche rendere opzionale l'applicazione della ConcreteStrategy, dotando il client di un comportamento di default inizialmente.
Iterator
Anche detto "cursore", fornisce un metodo per scorrere gli elementi di una struttura (lista, albero, grafo..) senza conoscerne i dettagli di implementazione.
Tipicamente una classe iterator espone un'interfaccia con i metodi:
- first(), vai al primo elemento
- next(), vai al prossimo elemento
- hasMore(), verifica se ci sono altri elementi successivi
- current(), restituisce l'elemento corrente
In questo pattern distinguiamo:
- una classe Struttura composta di più elementi
- una classe Iteratore usato per scorrere gli elementi della struttura
- una classe Client che possiede un riferimento all'iteratore per operare sugli elementi della struttura
Il separare l'oggetto iteratore dalla struttura stessa consente di poter cambiare l'implementazione della struttura senza inficiare le funzioni che operano iterando su essa. Tali funzioni possono anche non ricevere un riferimento alla struttura, ma direttamente un riferimento ad un suo iteratore.
Tuttavia è possibile anche che una classe rappresentante una struttura implementi essa stessa l'interfaccia di un iteratore, in questo caso parliamo di iteratore "interno" anziché dell'iteratore "esterno" che si ha ove questo risieda in una classe separata.
Poiché per attraversare una struttura è necessario conoscerne i dettagli, e si vuole evitare in generale che nella classe iteratore si inseriscano i dettagli implementativi della classe "struttura", è possibile che la classe "struttura" esponga dei metodi di attraversamento e che nell'iteratore sia usato per memorizzare lo stato di attraversamento, e per il resto richiami i metodi esposti dalla struttura. Questo è quel che accade di solito se la struttura è complessa ed è gestita da una classe "Composite" (vedasi
structural patterns)
Un altro uso degli iterator si ha nel caso in cui si voglia accedere agli elementi di una struttura in modi diversi, ossia seguendo diversi ordinamenti. In questo caso, è possibile creare un oggetto iteratore separatamente, con la logica di ordinamento/attraversamento desiderata, e poi utilizzarlo in una funzione che non dipenda più da quella scelta.
Mediator
Definisce un oggetto che incapsula la logica di interazione tra un insieme di altri oggetti.
Ha senso quando un insieme di oggetti interagisce con meccanismi complessi ma al contempo ben definiti. La logica di interazione viene centralizzata in un unico oggetto, il mediatore, semplificando il modello di interazione "molti a molti" con un modello "uno a molti" in cui ogni oggetto del sistema comunica solo col mediator.
Il mediator quindi migliora il disaccoppiamento (decoupling) tra le classi, e sposta la logica di interazione in un unica classe.
Altri vantaggi che si ottengono sono:
- poter cambiare la logica di interazione senza derivare nuove classi per ogni componente del sistema, infatti basterà cambiare (derivare) solo la classe mediator a tal fine
- astrarre e rendere quindi più "leggibile" la logica di interazione
Tuttavia centralizzare la logica di controllo rappresenta anche un costo da pagare, poiché è facile che la classe mediator diventi molto complessa e difficile da manutenere.
Classi mediator si trovano spesso nei framework di sviluppo, in cui si promuovono dei modelli di interazione, che agevolano la scrittura di componenti che sono poi inserite in un contesto globale in maniera relativamente semplice, dovendo comunicare solo col mediator con interfacce ben definite e collaudate.
Memento
Serve a catturare lo stato interno di un oggetto, senza violare i paradigmi di information-hiding, così che l'oggetto possa essere serializzato e deserializzato.
Il memento è utile anche per implementare meccanismi di "undo" in una sequenza di operazioni (vedasi il pattern Command ), ad esempio richiedendo un oggetto Memento ad ogni destinatario di un'operazione in modo che volendo tornare allo stato precedente, basterà inviare all'oggetto lo stato (precedentemente ottenuto) da assumere.
Ai fini dell'implementazione del memento, è dunque necessaria una classe che implementi un'interfaccia tipo:
interface IMemento {
object getState();
void setState(object state);
}
Mentre le classi "origine" di cui si vuole persistere lo stato esporranno un'interfaccia di tipo:
interface ISerializable {
void setMemento(IMemento m);
IMemento getMemento();
}
In questo modo l'oggetto che usa le classi "origine" attraverso i metodi getMemento e setMemento potrà impostarne lo stato senza conoscere i dettagli di come questo sia stato codificato.
Observer
Definisce una dipendenza uno a molti tra un oggetto "osservato" ed uno o più "osservatori" in modo tale che ad ogni modifica dell'oggetto osservato, gli osservatori ricevano una notifica automatica.
Si usa quando la modifica di un oggetto "osservato" deve avere degli effetti collaterali su altri oggetti che però devono essere trasparenti a chi effettua la modifica, ossia è attuato un decoupling tra chi agisce sull'oggetto per modificarlo e gli n "osservatori" che attueranno delle azioni in base alle modifiche.
E' ancora più utile se il numero degli osservatori può cambiare dinamicamente durante l'esecuzione. Ogni osservatore decide in autonomia di "registrarsi" o "deregistrarsi" agli eventi dell'oggetto osservato, e riceverà le notifiche (tipicamente tramite una callback oppure esponendo un'interfaccia IUpdate()) ogni volta che ce ne sarà bisogno.
L'Observer è un caso particolare dell'Inversion of Control, che agisce su ogni modifica dell'observed.
Le componenti di questo pattern sono:
Un oggetto "Observed" che espone l'interfaccia
interface IObserved {
object addObserver(IObserver o);
void removeObserver(IObserver o);
}
Uno o più oggetti "observer" che espongono l'interfaccia
interface IObserver {
void update(object o);
}
e l'oggetto osservato tiene traccia di tutti i propri osservatori ed ogni qualvolta è necessario invoca il metodo update() degli observer passando come parametro se stesso.
E' possibile adottare delle varianti per cui l'observer può decidere di registrarsi solo a determinate modifiche oppure nell'invocazione del metodo update si può specificare in qualche modo cosa sia cambiato, per facilitare il compito dell'observer.
Il prezzo da pagare è che una modifica apparentemente "innocua" all'oggetto observed può avere delle conseguenze anche di rilievo sugli observer, ed eventuali errori risultanti possono essere difficili da individuare.
State
Consente di cambiare comportamento di un oggetto in base al proprio stato interno, in sostanza il cambiamento di stato fa si che l'oggetto con cui si interagisce sia effettivamente diverso e anche appartenente ad una diversa classe.
Questo schema prevede un'interfaccia "IContext" che espone un insieme di metodi comuni a tutte le classi concrete considerate.
interface IContext {
void method1(..);
void method2(..);
..
void methodN(..);
}
C'è una classe Context che espone quest'interfaccia e conserva il riferimento "service" ad una classe concreta a cui "gira" le richieste di invocazione di tali metodi. In base al contesto, l'effettiva istanza a cui punta service può cambiare, e quindi anche l'implementazione dei suddetti metodi.
Nessun commento:
Posta un commento