I pattern strutturali sono relativi a come classi e oggetti sono composti per formare strutture più grandi.
I design pattern strutturali basati su classi utilizzano l’ereditarietà per generare classi che combinano le proprietà di classi base.
I design pattern strutturali basati su oggetti mostrano come comporre oggetti per realizzare nuove funzionalità. Danno flessibilità alla composizione che viene modificata a run-time, cosa impossibile con la composizione statica (con classi).
Alcuni structural patterns catalogati:
Esempi di structural patterns illustrati in dettaglio nel seguito:
Rendere più semplice l’uso di un (sotto)sistema.
Fornire un’unica interfaccia per un insieme di funzionalità “sparse” su più interfacce/classi.
La suddivisione di un sistema in sottosistemi aiuta a ridurre la complessità.
Tuttavia, occorre diminuire comunicazione e dipendenze tra i sottosistemi.
L’utilizzo di un oggetto façade fornisce un’unica e semplice interfaccia alle funzionalità del sottosistema.
Robot con quattro classi:
Quanto deve conoscere della meccanica l’operatore?
Si supponga di voler indivduare un oggetto e spostarlo in una locazione predefinita:
oldLocation = Camera.identify(object);
Arm.move(oldLocation);
Pliers.close();
Arm.move(newLocation);
Pliers.open();
Problema: non c’è incapsulamento: l’operatore deve conoscere la struttura ed il comportamento.
Utilizzare il pattern Façade:
Façade:
Classi del sottosistema:
Classi per disegno 3D per disegnare in 2D.
Compilatore:
Promuove un accoppiamento debole fra cliente e sottosistema.
Nasconde al cliente le componenti del sottosistema.
Il cliente può comunque, se necessario, usare direttamente le classi del sottosistema.
L’accoppiamento tra i client ed il sottosistema può essere ridotto anche rendendo Façade una classe astratta con sottoclassi concrete per diverse implementazioni di un sottosistema.
In tal caso, i client possono comunicare con il sottosistema attraveso l’interfaccia della classe astratta Façade.
Il sottosistema di compilazione definisce una classe BytecodeStream
che implementa un flusso di oggetti Bytecode
. Ciascun oggetto Bytecode
incapsula un bytecode che specifica le istruzioni macchina.
La classe Scanner
del sottosistema riceve un flusso di caratteri e genera un flusso di token, un token per volta.
Il Parser
effettua l’analisi dei token generati dallo Scanner
e genera un parse tree utilizzando un ProgramNodeBuilder
.
Il parse tree è fatto di istanze di sottoclassi di ProgramNode
.
ProgramNode
utilizza un oggetto CodeGenerator
per generare codice macchina nella forma di oggetti Bytecode
in un flusso BytecodeStream
.
La classe Compiler
è una “facciata” che mette tutti I pezzi insieme fornendo una semplice interfaccia per la compilazione del codice sorgente e la generazione del codice per una particolare macchina.
Il metodo Traverse
genera il codice macchina: Mostra codice
Ogni sottoclasse di ProgramNode (StatementNode, ExpressionNode, …) implementa il metodo Traverse da richiamare sui propri oggetti figli: Mostra codice
Adattare l’interfaccia di una classe già pronta all’interfaccia che il cliente si aspetta.
“Adapter” come l’”adattatore” per prese di corrente.
Talvolta una classe progettata per il riuso non è riusabile solo perché la sua interfaccia non coincide con quella del dominio specifico di un’applicazione.
Si consideri un editor di disegno che fornisce l’interfaccia Figura i cui metodi riempi e display sono implementati dalle classi Punto, linea e Quadrato.
Vogliamo aggiungere Cerchio
Lo implementiamo da zero?
Usiamo la classe Circle?
Creiamo un adattatore.
Usiamo Adapter quando:
Object Adapter
Class Adapter
Target
Client
Adaptee
Adapter
Class Adapter:
Object Adapter:
In alcuni linguaggi (Smalltalk) è possibile realizzare classi che includono un adattamento dell’interfaccia (pluggable adapter).
Se due client richiedono di vedere un oggetto in maniera diversa è possibile definire dei two-way adapters.
In C++, un class adapter può essere implementato facendo ereditare ad Adapter pubblicamente da Target e privatamente da Adaptee: in tal modo Adapter è un sottotipo di Target ma non di Adaptee.
Vediamo come implementare il diagramma sottostante sia per Class Adapter che per Object Adapter.
Shape e TextView sono uguali, ovviamente, in entrambi i casi.
Class Adapter
Mostra codice
La funzione BoundingBox converte l’interfaccia di TextView per renderla conforme a quella di Shape.
Mostra codicebool TextShape::IsEmpty () const {
return TextView::IsEmpty();
}
Si supponga che esista una classe TextManipulator per la manipolazione di un TextShape.
Manipulator* TextShape::CreateManipulator () const {
return new TextManipulator(this);
}
Object Adapter
Utilizza la composizione di oggetti per combinare classi con interfacce diverse.
In questo approccio l’adattatore TextShape mantiene un puntatore a TextView.
bool TextShape::IsEmpty () const {
return _text->IsEmpty();
}
CreateManipulator è uguale al caso del Class Adapter.
Entrambi sono “wrapper” (involucri).
Entrambi si basano su un’interfaccia, ma:
Aggiungere responsabilità ad un oggetto dinamicamente.
Alternativa (più flessibile) alla creazione di sottoclassi per l’estensione di funzionalità (anche la specializzazione può aggiungere responsabilità/funzionalità/operazioni).
Talvolta occorre aggiungere responsabilità a dei singoli oggetti, non ad un intera classe.
Si consideri ad es. l’interfaccia grafica di un programma di videoscrittura, alla quale si vuole aggiungere una barra di scorrimento e un bordo di contorno.
Ereditarietà
Contro
Racchiudere l’oggetto da “decorare” (l’area di testo) in un altro oggetto, responsabile di gestire la “decorazione” (il bordo, la scrollbar): il “decoratore”.
Il decoratore trasferisce/delega e fa qualcosa in più (prima o dopo).
Il decoratore ha l’interfaccia conforme all’oggetto decorato, e quindi è trasparente al cliente.
Decorator
Ereditarietà
Usiamo il Decorator quando:
Usi tipici
Componente grafica: area di testo, finestra, panel, …
decorata con: bordo, barra di scorrimento, … (eventualmente più di uno!)
Component (VisualComponent)
ConcreteComponent (TextView)
Decorator
ConcreteDecorator (BorderDecorator, ScrollDecorator)
Vantaggi;
Svantaggi:
L’interfaccia del Decorator deve essere conforme a quella del Component che decora.
L’interfaccia di Component deve essere leggera.
Nel caso in cui si voglia aggiungere un’unica responsabilità aggiuntiva non è necessario usare la classe astratta Decorator.
Si consideri ora un oggetto finestra nel quale poniamo il nostro component:
void Window::SetContents (VisualComponent* contents) {
// ...
}
Per cui possiamo scrivere:
Window* window = new Window;
TextView* textView = new TextView;
window->SetContents(textView);
Per avere una TextView con bordo e scrollbar:
window->SetContents(
new BorderDecorator(
new ScrollDecorator(textView), 1
)
);
2. La modellazione a oggetti e il linguaggio UML (richiami)
3. Generalità su Java e la programmazione ad oggetti
6. Regole di traduzione da UML a Java/C++
7. Programmazione multi-thread
8. Sincronizzazione tra thread
9. Programmazione client-server con socket TCP/IP (Java networkin...
10. Programmazione di applicazioni client-server: il Pattern Proxy...
12. Design Patterns
13. Pattern architetturali - Esempi
14. Design pattern creazionali. Esempi
15. Design pattern strutturali. Esempi
16. Introduzione alle tecnologie middleware
17. Modelli di middleware: RPC, MOM, TP, TS