Software Engineering a.y. 2019-2020
Introduzione ai pattern di design (parte 3) Prof. Luca Mainetti
Università del Salento
Obiettivi della lezione
! Continuare lo studio di caso sull’editor di testi, al fine di introdurre i seguenti pattern
– decorator: per decorare l’interfaccia utente (scrollbar, toolbar, ecc.) – abstract factory: per avere differenti modelli di look-and-feel
– bridge: per supportare differenti sistemi di window
Decorazioni dell’interfaccia utente
! Lexi utilizza varie “decorazioni” dell’interfaccia utente – i bordi della pagina
– le barre di scorrimento
! L’obiettivo progettuale è quello di rendere semplice e dinamica l’aggiunta e la rimozione di tali decorazioni
! Potremmo utilizzare un approccio basato sull’ereditarietà, poiché comunque ci è necessario estendere il codice esistente
– BorderedComposition = sottoclasse di Composition che aggiunge un bordo ad una Composition
– ScrollableComposition = sottoclasse di Composition che aggiunge una barra di scorrimento ad una Composition
– BorderedScrollableComposition = sottoclasse di Composition che aggiunge un bordo ed una barra di scorrimento ad una Composition
! Ma questo approccio non solo preclude gli interventi in esecuzione, ma fa esplodere il numero di classi necessarie per le decorazioni
Decorazioni dell’interfaccia utente (cont.)
! La composizione offre un meccanismo più flessibile
! Problema: quali oggetti devono essere composti?
– una decorazione viene applicata a un glyph, possiamo allora trattare la decorazione stessa come un oggetto (ad esempio, istanza della classe Border)
– i candidati per la composizione sono quindi Glyph e Border
! Problema successivo: chi compone e che contiene?
– se Glyph contiene Border dobbiamo rendere tutte le sottoclassi di Glyph consapevoli del Border
– se Border contiene Glyph ha senso poiché è così anche
nell’interfaccia utente. Inoltre tale scelta ha il vantaggio di delimitare tutto il codice per decorare con il bordo in una singola classe
Decorazioni dell’interfaccia utente (cont.)
! Il fatto che i bordi possono avere a loro volta un aspetto ci spinge a considerare le decorazioni dei particolari glyph (Border è una
sottoclasse di Glyph)
! In questo modo inoltre i client di glyph non dovrebbero trattare in modo differente i glyph decorati da quelli non decorati
! I client non sanno se stanno utilizzando un componente (ad esempio un glyph semplice) o un suo contenitore (ad esempio un glyph decorato), in particolare se il contenitore poi delega tutte le sue operazioni (ad
esempio il disegnarsi) al componente. Questo concetto è detto contenitore trasparente
! Il contenitore può anche estendere il comportamento componente, eseguendo operazioni prima e/o dopo la delega al componente stesso
! Il contenitore può aggiungere uno stato al componente
Decorazioni dell’interfaccia utente (cont.)
+Draw(in Window) Glyph
+Draw(in Window) MonoGlyph -component
1 1
+Draw(in Window) +DrawBorder(in Window)
Border
+Draw(in Window) Scroller
! Tutti i glyph che rappresentano
decorazioni sono contenitori trasparenti
! Aggiungiamo per questo scopo la classe MonoGlyph che rappresenta tutti i glyph con decorazioni
! MonoGlyph ha un riferimento ad un componente (unico) ed a questo inoltra tutte le richieste che riceve. E’ così
trasparente ai suoi client
! Ed esempio, così implementa l’operazione Draw
void MonoGlyph::Draw (Window* w) {
_component ->Draw(w);
}
Decorazioni dell’interfaccia utente (cont.)
! Le sottoclassi di MonoGlyph devono fornire
l’implementazione concreta di almeno una delle operazioni di delega al componente
! Ad esempio, Border::Draw prima invoca l’operazione del padre in modo da permettergli di disegnarsi (tutto il glyph componente ad eccezione del bordo), poi invoca
l’operazione privata DrawBorder per disegnare il bordo
void Border::Draw(Window* w) { MonoGlyph::Draw(w);
DrawBorder(w);
}
Decorazioni dell’interfaccia utente (cont.)
! Componiamo l’istanza di
Composition con un’istanza di
Scroller per aggiungere le barre di scorrimento e con un’istanza di Border per aggiungere il bordo
! A fianco è mostrata la struttura risultante
! Se invertiamo l’ordine di
composizione di Scroller e Border, otteniamo una pagina in cui scorre anche il bordo
! La composizione avviene tra un decoratore e un singolo glyph.
Complicheremmo inutilmente il
codice se la generalizzassimo a più glyph
colonna
riga riga
composition barre scorrimento
bordo
Il pattern Decorator
! Applica la tecnica del contenitore trasparente
! La decorazione non si riferisce solamente alle interfacce grafiche, ma a qualunque elemento che aggiunga
dinamicamente responsabilità ad un oggetto
! Ad esempio, provate ad ipotizzare qualche “decorazione semantica” della chat del WebTalk oppure degli avatar o degli oggetti condivisi
! I decoratori sono un’alternativa flessibile all’uso di sottoclassi come modo di estendere le funzionalità
! Il pattern è anche detto wrapper
Diversi standard di look-and-feel
! Si vuole facilitare il porting di Lexi su piattaforme hardware e software differenti
! Ogni piattaforma definisce il proprio standard di look-and-feel che
stabilisce come le applicazioni si presentano all’utente e in che modo reagiscono agli eventi
! Vogliamo che Lexi si adatti a look-and-feel diversi e che l’aggiunta di nuovi standard sia semplice
! Lexi utilizza il glyph per rappresentare sia oggetti visibili (caratteri, pulsanti, ecc.) sia oggetti invisibili (righe, colonne, ecc.)
! Gli standard di look-and-feel specificano come si comportano e come si visualizzano gli oggetti visibili (i widgets) che hanno responsabilità di controllo di altri glyph, cioè pulsanti, menu, scrollbar, ecc.
! I widget possono fare uso di glyph più semplici (caratteri, cerchi, poligoni) per presentare i dati
Diversi standard di look-and-feel (cont.)
! Potremmo avere un insieme di sottoclassi astratte di Glyph per ciascuna categoria di widget: ScrollBar, Button
! Per ciascuna sottoclasse astratta potremmo avere un insieme di
sottoclassi concrete che implementano i differenti standard di look-and- feel: MotifButton, PMButton, MacButton
! Quando Lexi dovrà visualizzare un pulsante in uno specifico look-and- feel dovrà istanziare la sottoclasse opportuna
! Lexi non può fare ciò con una chiamata diretta ad un costruttore, poiché sarebbe necessario inserire nel codice un particolare stile di pulsante, rendendo quindi impossibile modificare tale scelta in esecuzione
! Inoltre per portare Lexi su altre piattaforme sarebbe necessario modificare ogni costruttore
! Dobbiamo rendere astratto il processo di creazione degli oggetti
Diversi standard di look-and-feel (cont.)
! Ciò che vogliamo evitare è la chiamata diretta di costruttori
ScrollBar* sb = new MotifScrollBar;
! Supponiamo di fare nel seguente modo
ScrollBar* sb = guiFactory->CreateScrollBar();
! Dove guiFactory è un’istanza della classe MotifFactory, mentre CreateScrollBar() restituisce una nuova istanza della sottoclasse ScrollBar per lo specifico look-and-feel (Motif, in questo esempio)
! Per i client, l’effetto è il medesimo della chiamata diretta del costruttore, ma nel codice non ci sono più specifici
riferimenti a Motif
Diversi standard di look-and-feel (cont.)
! guiFactory astrae il processo di creazione per ogni look-and-feel ed anche per ogni tipologia di widget
! Grazie al fatto che MotifFactory è una sottoclasse di GUIFactory, che è una classe astratta che definisce l’interfaccia di creazione di glyph che rappresentano widget
! GUIFactory contiene tutte le operazioni per la creazione dei suoi prodotti
– CreateScrollBar() – CreateButton() – ecc.
! Le sottoclassi di GUIFactory implementano tali operazioni per uno
specifico look-and-feel in modo da restituire oggetti che sono istanze di MotifScrollBar, PMScrollBar, MotifButton, PMButton, ecc.
! Una factory crea prodotti correlati. In questo esempio, crea prodotti che rispondono allo stesso look-and-feel
Diversi standard di look-and-feel (cont.) factory
+CreateScrollbar() +CreateButton() +CreateMenu() +...()
GUIFactory
+CreateScrollbar() +CreateButton() +CreateMenu() +...()
MotifFactory
+CreateScrollbar() +CreateButton() +CreateMenu() +...()
PMFactory
+CreateScrollbar() +CreateButton() +CreateMenu() +...()
MacFactory
return new MotifScrollBar return new MotifButton return new MotifMenu
return new PMScrollBar return new PMButton return new PMMenu
return new MacScrollBar return new MacButton return new MacMenu
Diversi standard di look-and-feel (cont.) prodotto della factory
Glyph
+ScrollTo() ScrollBar
+Press() Button
+Popup() Menu
+ScrollTo() MotifScrollBar
+ScrollTo() PMScrollBar
+ScrollTo() MacScrollBar
+Press() MotifButton
+Press() MacButton
+Press() PMButton
+Popup() MotifMenu
+Popup() MacMenu
+Popup() PMMenu
Diversi standard di look-and-feel (cont.)
! Ultimo problema da risolvere: da dove arriva l’istanza di GUIFactory?
! Può essere una variabile globale, un membro static di una classe, una variabile locale se l’intera interfaccia utente
viene creata all’interno di una classe singola
! Esiste anche il pattern Singleton che risolve il problema di creare una sola istanza di una classe, come in questo caso (il look-and-feel concreto è uno solo)
! In ogni caso, guiFactory deve essere inizializzata prima che sia usata per creare i widget e dopo avere deciso il look-
and-feel desiderato
Diversi standard di look-and-feel (cont.)
! Se il look-and-feel è noto prima della compilazione
GUIFactory* guiFactory = new MotifFactory;
! Se l’utente può specificare il look-and-feel da riga di comando
GUIFactory* guiFactory;
const char* styleName = getenv(“LOOK_AND_FEEL”);
if (strcmp(styleName, “Motif”) == 0) { guiFactory = new MotifFactory;
} else if (strcmp(styleName, “PM”) == 0) { guiFactory = new PMFactory;
} else {
guiFactory = new DefaultGUIFactory;
}
! Potremmo utilizzare una tabella che mappa le stringhe su
oggetti factory, “registrando” nuove sottoclassi factory
Il pattern Abstract Factory
! Fornisce un modo generale per creare famiglie di oggetti prodotto correlati senza istanziare direttamente le classi prodotto
! E’ utile quando il numero e la tipologia dei prodotti rimane costante nel tempo, e quando ci sono reali differenze tra le famiglie
! La scelta di una famiglia di prodotti specifica viene fatta istanziando una factory concreta
! Il pattern permette di cambiare a runtime la famiglia di
prodotti, sostituendo l’istanza della factory concreta con
l’istanza di un’altra factory
Diversi window system
! Una classe Window visualizza un glyph o una struttura di glyph sullo schermo. Ha le seguenti responsabilità, che sono tipiche dei window system
– fornire operazioni per disegnare gli elementi grafici – inconizzare, de-iconizzare
– ridimensionare
– disegnare il contenuto su richiesta (ad esempio, perché la finestra è stata de-inconizzata, una finestra sovrapposta è stata iconizzata, ecc.)
! Il problema è che il window system sono fatti da produttori di software differenti e possono essere molto diversi
– la classe Window fornisce l’intersezione delle funzionalità: troppo limitata – la classe Window fornisce l’unione delle funzionalità: troppo grande e
probabilmente inconsistente
Diversi window system (cont.)
+Draw(in Window) Glyph
+Redraw() +Iconify() +Lower() +...() +DrawLine() +DrawPolygon() +DrawText() +...()
Window
ApplicationWindow
+Iconify() IconWindow
+Lower() DialogWindow glyph>Draw(this)
-owner
owner>Lower() -glyph
! Adottiamo un approccio intermedio dove la classe astratta Window definisce le caratteristiche più
comuni ai diversi window system
! Le sottoclassi concrete di Window supportano le differenti tipologie di
finestre di un medesimo windows system
! In questo modo abbiamo un’astrazione uniforme di un qualsiasi window
system
Diversi window system (cont.)
! A questo punto come entrano in gioco le finestre dipendenti dalla piattaforma utilizzata?
! Poiché non stiamo implementando un nuovo window system, in qualche punto le nostre astrazioni dovranno essere implementate dal window system
sottostante. Dove sono tali implementazioni?
! Potemmo implementare più versioni di Window e delle sue sottoclassi per ciascun window system. La corretta versione dovrà essere scelta all’atto della compilazione di Lexi per la corretta piattaforma
! Potremmo creare per ogni sottoclasse di Window delle altre sottoclassi che forniscono le diverse implementazioni per i differenti window system
! Nel primo caso abbiamo un problema di tenere traccia della corretta configurazione
! Nel secondo caso abbiamo un problema di esplosione del numero di classi
! In entrambi i casi non possiamo cambiare il window system in uso dopo che Lexi è stato compilato. Abbiamo diversi eseguibili
! La soluzione ancora una volta sta nell’incapsulare ciò che cambia, in questo caso l’implementazione del window system
Diversi window system (cont.)
+Redraw() +DrawRect(in ...)
Window
ApplicationWindow IconWindow DialogWindow
+DeviceRedraw() +DeviceRect(in ...)
WindowImp
+DeviceRedraw() +DeviceRect(in ...)
MacWindowImp -imp
+DeviceRedraw() +DeviceRect(in ...)
XWindowImp
+DeviceRedraw() +DeviceRect(in ...)
PMWindowImp
! Definiamo una gerarchia di classi separata che ha come radice la classe
astratta WindowImp che fornisce un’interfaccia per differenti implementazioni di window system
! Un oggetto window deve essere inizializzato con un riferimento ad un’istanza della sottoclasse di WindowImp che corrisponde alla piattaforma desiderata
Diversi window system (cont.)
! Le sottoclassi di WindowImp ricevono richieste e le convertono in operazioni specifiche del particolare window system
! Ad esempio, un Rectangle delega il disegno ad una Window
void Rectangle::Draw (Window* w) { w->DrawRect(_x0, _y0, _x1, _y1);
}
! L’implementazione di DrawRect usa l’operazione astratta DeviceRect() di WindowImp
void Window::DrawRect (Coord x0, y0, x1, y1) { _imp->DeviceRect(x0, y0, x1, y1);
}
dove _imp è una variabile membro di Window che memorizza l’istanza della sottoclasse di WindowImp con cui è stata configurata
Diversi window system (cont.)
! Per la classe XWindowImp l’implementazione di DeviceRect() può essere la seguente
void XWindowImp::DeviceRect (Coord x0, y0, x1, y1) { int x = round(min(x0, x1));
int y = round(min(y0, y1));
int w = round(abs(x0 - x1));
int h = round(abs(y0 – y1));
XDrawRectangle(_dpy, _winid, _gc, x, y, w, h);
}
dove XDrawRectangle() è l’operazione fornita dalla piattaforma X per disegnare un rettangolo intermini del suo vertice inferiore di sinistra, della sua lunghezza e della sua altezza
! Altre classi concrete potrebbero implementare l’operazione DeviceRect() in modo profondamente differente
Diversi window system (cont.)
! Poiché PM non definisce operazioni esplicite per disegnare rettangoli, l’implementazione di DeviceRect() in PMWindowImp può essere
void PMindowImp::DeviceRect (Coord x0, y0, x1, y1) { Coord left = min(x0, x1);
Coord right = max(x0, x1);
Coord bottom = min(y0, y1);
Coord top = max(y0, y1);
PPOINTL point[4];
point[0].x = left; point[0].y = top;
point[1].x = right; point[1].y = top;
point[2].x = right; point[2].y = bottom;
point[3].x = left; point[3].y = bottom;
if ((GpiBeginPath(_hps, 1L) == false) ||
(GpiSetCurrentPosition(_hps, &point[3]) == false) ||
(GpiPolyLine(_hps, 4L, point) == GPI_ERROR) ||
(GpiEndPath(_hps) == false)) { // segnala errore
} else { GpiStrokePath(_hps, 1L, 0L); }
Diversi window system (cont.)
! Una window richiede di essere inizializzata in modo appropriato con un’istanza della classe WindowImp. Come facciamo ad inizializzare correttamente _imp?
! Possiamo definire una classe WindowSystemFactory che fornisce un’abstract factory per la creazione di differenti tipologie di oggetti che forniscono
l’implementazione nelle diverse piattaforme Class WindowSystemFactory {
public:
virtual WindowImp* CreateWindowImp() = 0;
virtual ColorImp* CreateColorImp() = 0;
virtual FontImp* CreateFontImp() = 0;
};
! Creiamo poi una factory concreta per ogni window system
Class PMWindowSystemFactory : public WindowSystemFactory {public:
virtual WindowImp* CreateWindowImp() { return new PMWindowImp; }
……… };
Diversi window system (cont.)
! Altra factory concreta
Class XWindowSystemFactory : public WindowSystemFactory {
public:
virtual WindowImp* CreateWindowImp() { return new XWindowImp; }
……… };
! Il costruttore di Window inizializza _imp con l’istanza di WindowImp appropriata
Window::Window () {
_imp = windowSystemFactory->CreateWindowImp();
};
! La variabile windowSystemFactory referenzia un’istanza nota di una sottoclasse di WindowSystemFactory
Il pattern Bridge
! L’interfaccia Window tiene conto del programmatore applicativo
! L’interfaccia WindowImp tiene conto del programmatore sistemista e dei differenti window system
! Le relazioni tra Window e WindowImp sono un esempio di applicazione del pattern Bridge
! Tale pattern ha lo scopo di consentire a gerarchie separate di classi di cooperare anche se evolvono in maniera indipendente
! Il pattern Bridge ci permette di mantenere e far evolvere le finestre
logiche senza dover agire sul codice specifico delle finestre del window system
! Il pattern Bridge disaccoppia quindi un’astrazione dalla sua
implementazione, permettendo di farle evolvere indipendentemente
! Utile quando un’astrazione può avere più implementazioni diverse