Introduzione ai Design Patterns



Scaricare 109.16 Kb.
15.12.2017
Dimensione del file109.16 Kb.


Introduzione ai Design Patterns.

Progettare software object-oriented riusabile è difficile: non “viene alla prima” e bisogna modificarlo più volte prima di ottenere la versione finale.

Tuttavia i progettisti esperti sviluppano dei buoni progetti object-oriented. Cosa sanno in più rispetto ai progettisti principianti?
Gli esperti non risolvono mai i problemi da zero, ma quando trovano una buona soluzione la riusano nei progetti successivi. Essi, cioè, usano dei design patterns.
I design pattern di cui parliamo noi, in particolare, hanno lo scopo di poter essere usati da tutti, non solo da chi ha trovato il modello durante la propria esperienza personale di progettazione.
Il libro
Erich Gamma - Richard Helm - Ralph Johnson - John Vlissides

Design Patterns - Elements of Reusable Object - Oriented Software


Addison-Wesley , 1995
fornisce un catalogo di modelli usabili.
In questo libro, naturalmente, non viene detto tutto quello che un progettista esperto deve sapere, ma i design pattern qui descritti possono già aiutare un progettista ad ottenere più velocemente un design corretto.

Che cos’è un Design Pattern ?
“Ogni pattern descrive un problema che si ripresenta e descrive la soluzione in modo tale da poterla riusare più volte senza rifarla” (Christopher Alexander).

Ogni design pattern è caratterizzato da quattro elementi essenziali:




  1. Nome del pattern:

Ogni pattern ha un nome univoco che richiama il problema che il design pattern risolve e la sua soluzione. NOTA: scegliere bene i nomi aiuta a far crescere il “vocabolario” e conseguentemente il linguaggio del progettista (migliore comunicazione con colleghi, nella documentazione, ecc). Analogia con il problema del “naming” di tipi e variabili di un programma.


  1. Problema:

Per ciascun design pattern viene detto quando è bene applicarlo e cioè in quale contesto ed in quali ipotesi.


  1. Soluzione:

Descrive gli elementi costituenti il progetto, le relazioni, responsabilità e collaborazioni. Viene data una descrizione dello schema che risolve il problema ed una struttura del modello.


  1. Conseguenze:

Vengono descritti i risultati e i compromessi a cui porta il design pattern considerato, i costi in termini di occupazione di memoria e tempo da sostenere qualora si faccia uso del pattern, la sua flessibilità, estensibilità o portabilità. Tutte queste considerazioni aiutano a decidere se e quale pattern scegliere.
I design pattern non sono specifici per un linguaggio di programmazione.

La scelta del linguaggio da usare va fatta tenendo conto di quale, tra i linguaggi disponibili, esprime il pattern considerato più facilmente nel contesto applicativo e dei requisiti.


La sola notazione grafica non è sufficiente a rappresentare un pattern perché essa non ne rappresenta l’intero processo decisionale. Sono necessarie anche altre informazioni per descrivere un design pattern in modo completo:

- Scopo del pattern

- Esempio di situazione concreta risolvibile con il pattern

- Applicabilità del pattern

- Struttura del pattern

- Classi e oggetti partecipanti al design pattern e loro responsabilità

- Collaborazioni tra i partecipanti

- Conseguenze

- Suggerimenti per l’implementazione

- Esempi di codice

- Esempi di uso in sistemi reali

- Pattern correlati al pattern considerato.



Il catalogo dei pattern secondo Gamma et al (“Group of Four” - GoF):





Purpose



Creational



Structural


Behavioral

Scope

Class




Factory Method


Adapter (class)


Interpreter

Template Method



Object

Abstract Factory

Builder

Prototype



Singleton

Adapter (object

Bridge

Composite



Decorator

Facade


Flyweight

Proxy

Chain of Responsability

Command


Iterator

Mediator


Memento

Observer


State

Strategy


Visitor


I pattern vengono classificati con due criteri:


1. Purpose: che cosa fa il pattern:

- Creazione di oggetti (Creational Patterns);

- Composizione di classi o oggetti (Structural Patterns);

- Modo in cui classi o oggetti interagiscono e distribuzione di responsabilità

(Behavioral Patterns).
2. Scope: specifica se il pattern va applicato a classi o a oggetti.

- I Class Patterns trattano le relazioni tra classi e sottoclassi, ad es. relazioni di ereditarietà (inheritance) statiche, cioè fissate al momento della compilazione (compile-time);

- Gli Object Patterns trattano le relazioni tra oggetti, che possono essere cambiate a

run-time e possono essere dinamiche.


Class Patterns

- Creational rimandano alcune parti della creazione dell’oggetto alle sottoclassi;

- Structural usano l’inheritance per comporre le classi;

- Behavioral usano l’inheritance per descrivere algoritmi e flussi di controllo.


Object Patterns

- Creational rimandano alcune parti della creazione dell’oggetto ad un altro oggetto;

- Structural descrivono modi per assemblare oggetti;

- Behavioral descrivono come un gruppo di oggetti coopera per eseguire un compito (task)

che nessun oggetto singolo può effettuare da solo.


Alcuni pattern sono usati insieme ad altri pattern.

Alcuni pattern sono alternativi ad altri pattern.

Alcuni pattern sono simili ad altri pattern.

Alcuni problemi le cui soluzioni sono migliorate attraverso l’adozione dei design pattern



Trovare gli oggetti appropriati
I programmi object-oriented si basano sul concetto di oggetto. Un oggetto (object) contiene i dati e le procedure che operano su tali dati. Le procedure sono tipicamente chiamate metodi (methods) o operazioni (operations). Un oggetto esegue un’operazione quando riceve una richiesta (request) o messaggio (message) da un client.
Le richieste sono il solo modo per far eseguire un’operazione da un oggetto. Le operazioni sono il solo modo per cambiare un dato interno di un oggetto. A causa di queste restrizioni, lo stato interno dell’oggetto viene detto essere incapsulato (encapsulated), cioè non è possibile accedere direttamente ad esso e la sua rappresentazione è invisibile dall’esterno dell’oggetto.
Il difficile dell’object-oriented è capire con quale criterio suddividere il sistema in oggetti in quanto, ai fini della scelta, intervengono molti fattori (incapsulazione, granularità, flessibilità, riusabilità, eccetera). Esistono diversi approcci:


  1. stabilire che i nomi (della descrizione a parole del problema) sono le classi e i verbi le operazioni;




  1. focalizzare collaborazioni e responsabilità delle varie parti del sistema;




  1. creare il modello partendo dal mondo reale. A volte, però, il modello astratto non ha una controparte reale. Inoltre la realtà modellata rigorosamente oggi, potrebbe non esistere più domani. I design devono essere flessibili.

I pattern aiutano a identificare le astrazioni meno ovvie perché sono frutto di scoperte successive già fatte da chi ha progettato i pattern rendendoli più flessibili e riusabili.

Determinare la granularità dell’oggetto
Gli oggetti possono variare enormemente in dimensione e numero. Possono essere la rappresentazione di un pezzo hardware o dell’intera applicazione software.

Come decidiamo che cosa dovrebbe essere un oggetto?

I design pattern ci indirizzano in queste decisioni.

Specificare l’interfaccia dell’oggetto


Ogni operazione dichiarata da un oggetto specifica la propria signature, cioè il nome dell’operazione, gli oggetti che ha come parametri e il valore restituito dalle operazioni. L’insieme di tutte le signatures definite dalle operazioni di un oggetto è chiamata interfaccia (interface) dell’oggetto. Un’interfaccia di un oggetto caratterizza l’insieme completo di richieste che possono essere inviate all’oggetto. Qualunque richiesta che si accordi con la signature nell’interfaccia dell’oggetto può essere inviata all’oggetto.
Un tipo (type) è un nome usato per denotare una particolare interfaccia. Per esempio un oggetto è di tipo “Window” se accetta tutte le richieste per le operazioni definite nell’interfaccia chiamata “Window”. Un oggetto può avere molti tipi e oggetti differenti possono condividere uno stesso tipo. Una parte dell’interfaccia di un oggetto può essere caratterizzata da un tipo e altre parti da altri tipi. Due oggetti dello stesso tipo hanno bisogno di condividere solo alcune parti delle loro interfacce. Le interfacce possono contenere altre interfacce come sottoinsiemi. Un tipo è un sottotipo (subtype ) di un altro se la sua interfaccia contiene l’interfaccia del suo supertipo (supertype). Spesso un sottotipo eredita l’interfaccia del suo supertipo.
Gli oggetti sono conosciuti e usabili solo attraverso le loro interfacce. Clienti non sanno nulla della loro implementazione. Oggetti con implementazioni diverse possono avere la stessa interfaccia.
Quando una richiesta viene inviata ad un oggetto, la particolare operazione che viene eseguita dipende sia dalla richiesta che dall’oggetto che la riceve. Oggetti differenti che supportano richieste identiche possono avere implementazioni diverse delle operazioni che soddisfano queste richieste. L’associazione in fase di esecuzione (run-time) di una richiesta ad un oggetto e ad una delle sue operazioni è detta dynamic binding (legame dinamico).
Dynamic binding significa che una richiesta inviata viene affidata ad una particolare implementazione a run-time e non in fase di compilazione. In questo modo è possibile scrivere programmi che prevedono un oggetto con una particolare interfaccia, sapendo che qualunque oggetto che abbia l’interfaccia corretta accetterà la richiesta. Inoltre il dynamic binding permette di sostituire oggetti che hanno interfacce identiche a run-time, cioè dinamicamente. Questa sostituibilità è detta polimorfismo (polymorphism) ed è un concetto chiave nei sistemi object-oriented. Esso permette ad un oggetto client di fare poche assunzioni su altri oggetti, oltre a quella di supportare una particolare interfaccia. Il polimorfismo semplifica le definizioni dei client, disaccoppia gli oggetti tra di loro e permette loro di variare le proprie relazioni con ogni altro oggetto a run-time.
I design patterns aiutano a definire le interfacce identificando i loro elementi chiave e i tipi di dati che vengono passati attraverso di esse e possono consigliare cosa non deve essere inserito nelle interfacce stesse.

I design pattern definiscono anche le relazioni tra le interfacce. Spesso richiedono ad alcune classi di avere interfacce simili oppure inseriscono qualche restrizione su di esse.

Specificare le implementazioni dell’oggetto
Un’implementazione di un oggetto è definita dalla sua classe (class), che specifica i dati e la rappresentazione interni dell’oggetto e definisce le operazioni che l’oggetto può eseguire.
La notazione OMT / UML rappresenta una classe come un rettangolo con il nome della classe in grassetto. I dati che la classe definisce e le operazioni sono scritti in caratteri normali sotto il nome della classe. Il nome della classe, i dati e le operazioni sono separati da una linea.

Gli oggetti sono creati istanziando (instantiating) una classe. Si dice che l’oggetto è un’istanza (instance) della classe. Il processo di instanziazione di una classe alloca la memoria per i dati interni dell’oggetto (fatti da instance variables) e associa le operazioni a questi dati. Istanziando più volte una classe è possibile creare molte istanze simili di un oggetto.


Una freccia tratteggiata indica una classe che istanzia oggetti di un’altra classe. La freccia punta alla classe degli oggetti istanziati.
Possono essere definite nuove classi in termini di classi già esistenti usando l’ereditarietà delle classi (class inheritance). Quando una sottoclasse (subclass) eredita da una classe padre ( parent class ), include le definizioni di tutti i dati e le operazioni che la classe padre definisce. Gli oggetti che sono istanze della sottoclasse conterranno tutti i dati definiti dalla sottoclasse e dalla sua classe padre, e saranno in grado di eseguire tutte le operazioni definite da questa sottoclasse e dai suoi padri. Le relazioni tra le sottoclassi sono indicate con una linea verticale e un triangolo.
Lo scopo principale di una classe astratta (abstract class) è di definire un’interfaccia comune per le sue sottoclassi. Una classe astratta rimanderà alcune o tutte le sue implementazioni a operazioni definite nelle sottoclassi; quindi una classe astratta non può essere istanziata. Le operazioni che una classe astratta dichiara ma non implementa sono chiamate operazioni astratte (abstract operations). Le classi che non sono astratte sono dette classi concrete (concrete classes).
Le sottoclassi possono raffinare e ridefinire i comportamenti delle loro classi padre. Cioè una classe può sovrascrivere (override) un’operazione definita dalla sua classe padre. L’overriding dà alle sottoclassi la possibilità di gestire le richieste al posto delle loro classi padre. L’ereditarietà delle classi permette di definire le classi semplicemente estendendo altre classi, rendendo facile definire famiglie di oggetti aventi funzionalità correlate.
I nomi delle classi astratte vengono scritti in corsivo per distinguerle dalle classi concrete. Il corsivo è anche usato per denotare le operazioni astratte. Un diagramma può includere pseudocodice per un’implementazione di un’operazione; in tal caso il codice verrà scritto in una “pagina con un’orecchia” collegata all’operazione che implementa da una linea tratteggiata.
Una mixin class è una classe che ha lo scopo di fornire un’interfaccia o una funzionalità opzionale ad altre classi. E’ simile ad una classe astratta in quanto non deve essere istanziata. Le mixin classes richiedono ereditarietà multipla:

ExistingClass

Mixin



AugmentedClass

Una classe definisce come l’oggetto è implementato. La classe definisce lo stato interno dell’oggetto e l’implementazione delle sue operazioni.


Un tipo di un oggetto fa riferimento soltanto alla sua interfaccia, cioè l’insieme di richieste alle quali può rispondere.
Un oggetto può avere molti tipi e oggetti di classi differenti possono avere lo stesso tipo. C’è una stretta relazione tra classe e tipo. Poiché una classe definisce le operazioni che un oggetto può eseguire, essa definisce anche il tipo dell’oggetto. Dicendo che un oggetto è un’istanza di una classe, sottintendiamo che l’oggetto supporta l’interfaccia definita dalla classe.
L’ereditarietà delle classi (class inheritance) definisce un’implementazione di un oggetto in termini di un’altra implementazione di un oggetto, in breve è un meccanismo per la condivisione di codice e rappresentazione.
L’ereditarietà dell’interfaccia (interface inheritance o subtyping) descrive quando un oggetto può essere usato al posto di un altro.
Molti linguaggi di programmazione non supportano questa distinzione. Tuttavia, nella pratica, le persone fanno questa distinzione e molti dei design pattern dipendono da essa.
L’ereditarietà delle classi è un meccanismo per estendere una funzionalità di un’applicazione riusando le funzionalità delle classi parent (che di solito sono classi astratte). Essa permette di definire rapidamente un nuovo tipo di oggetto, in termini di un oggetto già esistente e consente di ottenere nuove implementazioni ereditando la maggior parte di ciò di cui si ha bisogno dalle classi già esistenti.
La possibilità di definire famiglie di oggetti con la stessa interfaccia (usualmente ereditandola da una classe) è importante, poiché il polimorfismo si basa su questo. Quando l’ereditarietà è usata correttamente, tutte le classi che derivano dalla classe astratta condividono la sua interfaccia. Questo vuol dire che una sottoclasse aggiungerà o modificherà operazioni ma non ne cancellerà. Quindi tutte le classi che possono rispondere ad una richiesta dell’interfaccia della classe astratta sono ottenute come sottoclasse della classe astratta stessa.
La manipolazione degli oggetti esclusivamente in termini dell’interfaccia definita dalle classi astratte ha due vantaggi:


  1. I client sono inconsapevoli del tipo specifico di oggetti che usano, finché gli oggetti aderiscono all’interfaccia che i clients si aspettano;




  1. I client sono inconsapevoli delle classi che implementano questi oggetti. I client conoscono solo le classi astratte che definiscono l’interfaccia.

Questo riduce enormemente le dipendenze di implementazione tra sottosistemi e ciò conduce al seguente principio di riusabilità del progetto object-oriented:


Si programma un’interfaccia, non un’implementazione.
In base a questo principio non vengono dichiarate variabili che devono essere istanziate da particolari classi concrete. Invece, si fa riferimento solo a un’interfaccia definita da una classe astratta. Questo è un concetto comune dei design pattern.
Da qualche parte, nel sistema, occorre istanziare classi concrete (cioè specificare una particolare implementazione) e i creational patterns permettono di fare proprio questo. Astraendo il processo di creazione dell’oggetto, questi patterns forniscono modi differenti per associare un’interfaccia alla sua implementazione in modo trasparente all’istanziazione. I creational patterns assicurano che il sistema sia scritto in termini di interfacce, non in termini di implementazioni.

Riuso
La maggior parte delle persone possono capire concetti come oggetti, interfacce, classi e ereditarietà.

La vera difficoltà sta nell’applicare questi concetti per costruire software flessibile e riusabile. I design patterns possono mostrare come far questo.


Le due più comuni tecniche per riusare le funzionalità in sistemi object-oriented sono l’ereditarietà delle classi e la composizione degli oggetti (object composition). Come abbiamo spiegato, l’ereditarietà delle classi permette di definire l’implementazione di una classe in termini di un’altra. Spesso si fa riferimento al riuso per subclassing come white-box reuse. Il termine “white-box” fa riferimento alla visibilità: con l’ereditarietà, l’interno delle classi padre è spesso visibile alle sottoclassi.
La composizione dell’oggetto è un’alternativa all’ereditarietà della classe. Qui la nuova funzionalità è ottenuta assemblando o componendo oggetti per ottenere funzionalità più complesse.

La composizione degli oggetti richiede che gli oggetti che devono essere composti abbiano interfacce ben definite. Questo stile di riuso è chiamato black-box reuse, perché nessun dettaglio interno degli oggetti è visibile. Gli oggetti appaiono solo come “scatole nere”.


L’ereditarietà e la composizione hanno vantaggi e svantaggi. L’ereditarietà delle classi è definita staticamente al momento della compilazione ed è facile da usare, dal momento che è supportata direttamente dal linguaggio di programmazione. Anche l’ereditarietà delle classi rende facile modificare l’implementazione che deve essere riusata.
Ma l’ereditarietà delle classi ha anche qualche svantaggio.

- non è possibile cambiare le implementazioni ereditate dalle classi padre a run-time, perché l’ereditarietà è definita al momento della compilazione.



- le classi padre spesso definiscono un comportamento minimo della rappresentazione fisica delle loro sottoclassi. Poiché l’ereditarietà espone le sottoclassi a dettagli dell’implementazione dei loro parent, è spesso detto che “l’ereditarietà rompe l’incapsulazione”. L’implementazione di una sottoclasse diventa così limitata dall’implementazione della sua classe parent ed ogni cambiamento nell’implementazione del parent forzerà il cambiamento della sottoclasse.
Le dipendenze dell’implementazione possono causare problemi quando si prova a riusare una sottoclasse. Se qualche aspetto dell’implementazione ereditata non dovesse essere appropriato per i domini dei nuovi problemi, la classe parent dovrebbe essere riscritta o sostituita da qualcosa di più appropriato. Questa dipendenza limita la flessibilità e infine la riusabilità. Una cura per questo è ereditare solo dalle classi astratte, dal momento che esse di solito forniscono poca o nessuna implementazione.
La composizione degli oggetti è definita dinamicamente al momento dell’esecuzione attraverso oggetti che acquisiscono riferimenti ad altri oggetti. La composizione richiede che gli oggetti rispettino le interfacce altrui, che a loro volta richiedono una progettazione attenta dell’interfaccia e in modo tale da non fermare l’utilizzo dell’oggetto con gli altri. Ma poiché agli oggetti si può accedere solo attraverso le loro interfacce, non possiamo rompere l’incapsulazione. Qualunque oggetto può essere sostituito a run-time da un altro dal momento che esso è dello stesso tipo. In più, poiché un’implementazione di un oggetto sarà scritta in termini di interfacce dell’oggetto, ci sono sostanzialmente meno dipendenze di implementazione.
La composizione degli oggetti ha un altro effetto sul progetto del sistema. Favorisce la composizione dell’oggetto rispetto all’ereditarietà della classe ed aiuta a tenere ogni classe incapsulata e focalizzata su un compito. Le classi e le gerarchie di classi rimarranno piccole e saranno meno agevolate a crescere fino a diventare mostri non gestibili. D’altra parte, un progetto basato sulla composizione dell’oggetto avrà più oggetti (e meno classi) e il comportamento del sistema dipenderà dalle loro relazioni invece di essere definito in una classe.
Questo ci conduce al nostro secondo principio del progetto object-oriented:

Favorire la composizione dell’oggetto rispetto all’ereditarietà della classe.
Idealmente, non occorre creare nuovi componenti per realizzare la riusabilità. Bisogna essere abili ad ottenere tutte le funzionalità di cui si ha bisogno assemblando i componenti esistenti attraverso la composizione dell’oggetto. Ma questo è un caso raro, perché l’insieme di componenti disponibili non è mai abbastanza ricco, in casi reali. Riusare grazie all’ereditarietà rende semplice creare nuovi componenti che possono essere composti con i vecchi. L’ereditarietà e la composizione dell’oggetto, quindi, lavorano insieme.
I progettisti tendono a esagerare nell’uso della ereditarietà come tecnica di riuso: i progetti sono invece spesso più riusabili (e più semplici) se si fanno dipendere maggiormente dalla composizione degli oggetti. Vedremo la composizione dell’oggetto applicata nei design patterns.
Delegation
La delegation è un modo per fare composizioni così potenti per il riuso come l’ereditarietà. Nella delegation, due oggetti sono coinvolti nella gestione di una richiesta: un oggetto ricevente (receiver) delega delle operazioni al suo delegato (delegate). Questo è analogo all’invio delle richieste da parte delle sottoclassi alle classi parent. Con l’ereditarietà, un’operazione ereditata può sempre riferirsi all’oggetto che riceve attraverso la variabile membro this del C++ e self in Smalltalk. Per ottenere lo stesso effetto con la delegation, il ricevitore passa se stesso al delegato per fare in modo che le operazioni delegate facciano riferimento al receiver.
Per esempio, invece di fare la classe Window come una sottoclasse di Rettangolo (poiché le finestre spesso sono rettangolari), la classe Window potrebbe riusare il comportamento di Rettangolo avendo una variabile istanza di Rettangolo e delegando uno specifico comportamento di Rettangolo ad essa. In altre parole, non abbiamo una Window che è un rettangolo, ma ha (contiene) un Rettangolo. Window deve ora inviare esplicitamente le richieste alla sua istanza di Rettangolo, invece prima avrebbe ereditato queste operazioni.

Il diagramma OMT/UML nella figura seguente realizza quanto descritto:



Window
Area() { return rectangle->Area() }

Rectangle


Area() { return width*height }

width


height

rectangle



Nella notazione OMT/UML una freccia indica che una classe tiene un riferimento ad un’istanza di un’altra classe. Il riferimento ha, opzionalmente, un nome. Nell’esempio considerato potrebbe essere “rectangle”.


Il maggior vantaggio della delegation è che essa rende semplice comporre comportamenti a run-time e cambiarne il modo con cui essi sono composti. La nostra finestra può diventare circolare a run-time semplicemente sostituendo la sua istanza Rettangolo con un’istanza Cerchio, assumendo che Rettangolo e Cerchio abbiano lo stesso tipo.
La delegation ha uno svantaggio che condivide con altre tecniche che rendono il software più flessibile attraverso la composizione degli oggetti: i software dinamici, altamente parametrizzati sono più difficili da capire rispetto ai software più statici. Ci sono anche inefficienze in fase di run-time, ma le inefficienze umane sono più importanti a lungo termine. La delegation è una buona scelta di progetto solo quando semplifica più di ciò che complica. Non è facile dare regole che dicano esattamente quando usare la delegation, perché quanto usarla dipenderà dal contesto e da quanta esperienza il progettista ha con essa. La delegation lavora meglio quando è usata in modi altamente stilizzati, cioè nei pattern standard.
La delegation è un esempio estremo di composizione di oggetti. Essa mostra che è sempre possibile sostituire l’ereditarietà con la composizione degli oggetti come meccanismo per il riuso di codice.

Ereditarietà Vs tipi parametrizzati


Un’altra (non strettamente object-oriented) tecnica per il riuso di funzionalità è quella dei tipi parametrizzati (parameterized types), anche conosciuti come generic (in Ada ed Eiffel) e template (in C++). Questa tecnica permette di definire un tipo senza specificare tutti gli altri tipi che esso usa: i tipi non specificati sono forniti come parametri nel punto in cui vengono usati. Per esempio, una classe Lista può essere parametrizzata dal tipo di elementi che essa contiene. Per dichiarare una lista di interi, occorre fornire il tipo “intero” come parametro del tipo parametrizzato Lista.

Per dichiarare una lista di oggetti Stringa, occorre fornire il tipo “Stringa” come parametro. L’implementazione del linguaggio creerà una versione personalizzata della classe parametrizzata Lista per ogni tipo di elemento.


I tipi parametrizzati ci danno un terzo modo (assieme all’ereditarietà delle classi e alla composizione degli oggetti) per comporre comportamenti nei sistemi object-oriented. Molti progetti possono essere implementati usando una qualunque di queste tre tecniche. Per parametrizzare un algoritmo di ordinamento (sorting) riguardo l’operazione che usa per confrontare gli elementi, possiamo fare il confronto come:

  • un’operazione implementata dalle sottoclassi;

  • la responsabilità di un oggetto passato all’algoritmo di ordinamento;

  • argomento di un template del C++ o un generic di Ada che specifica il nome della funzione da chiamare per confrontare gli elementi.


Ci sono importanti differenze tra queste tecniche: La composizione degli oggetti permette di cambiare il comportamento a run-time, ma essa richiede anche l’indirezione e può essere meno efficiente. L’ereditarietà permette di fornire implementazioni di default per le operazioni e permette alle sottoclassi di sovrascriverle. I tipi parametrizzati permettono di cambiare i tipi che le classi possono usare. Ma né l’ereditarietà, né i tipi parametrizzati possono cambiare a run-time. Quale approccio sia il migliore dipende dal progetto e dai vincoli dell’implementazione.
I tipi parametrizzati non sono necessari in tutti i linguaggi; Smalltalk, ad esempio, non ha un controllo del tipo (type checking) al momento della compilazione.


Strutture Run-Time e Strutture Compile-Time

Una struttura a run-time di un programma object-oriented spesso assomiglia alla sua struttura di codice. La struttura del codice è congelata al momento della compilazione: essa consiste di classi in relazione di ereditarietà fissata. Una struttura del run-time di un programma consente di cambiare rapidamente la rete di comunicazione tra gli oggetti. Le due strutture sono largamente indipendenti.


Consideriamo la distinzione tra aggregazione (aggregation) di oggetti e conoscenza (acquaintance) di oggetti e come differentemente essi manifestano se stessi al momento della compilazione e al momento dell’esecuzione. L’aggregazione implica che un oggetto possieda o sia responsabile di un altro oggetto. Generalmente diciamo che un oggetto ha (having) o è parte di (part of) di un altro oggetto. L’aggregazione implica che un oggetto aggregato e il suo possessore abbiano lo stesso periodo di vita.
La conoscenza implica che un oggetto sia a conoscenza (knows of) dell’altro oggetto. Qualche volta la conoscenza è chiamata “associazione” (association) o relazione di “uso” (using relationship). Gli oggetti conosciuti possono richiedere operazioni di tutti gli altri oggetti, ma essi non sono responsabili di nessun altro oggetto. La conoscenza è una relazione più debole dell’aggregazione e suggerisce accoppiamenti meno forti tra gli oggetti.
Nei nostri diagrammi, una freccia denota la conoscenza. Una freccia con un rombo alla sua base denota aggregazione.
È’ facile confondere l’aggregazione e la conoscenza perché esse sono spesso implementate nello stesso modo. In Smalltalk tutte le variabili sono riferimenti ad altri oggetti. Non c’è distinzione nel linguaggio di programmazione tra aggregazione e conoscenza. In C++ l’aggregazione può essere implementata definendo variabili membro che sono istanze reali, ma è più comune definirle come puntatori o riferimenti alle istanze. La conoscenza è anch’essa implementata con puntatori e riferimenti.
Infine la conoscenza e l’aggregazione sono determinate più dall’intento che da meccanismi espliciti di linguaggio. La distinzione può essere difficile da vedere nelle strutture di compile-time, ma è importante. Le relazioni di aggregazione tendono ad essere di meno e più permanenti rispetto a quelle di conoscenza. La conoscenza, d’altra parte, è fatta e rifatta più frequentemente, qualche volta esistendo solo per la durata di un’operazione. Le conoscenze sono anche più dinamiche e più difficili da distinguere nel codice sorgente.
Con questa disparità tra una struttura di un programma run-time e compile-time, è chiaro che il codice non rivela ogni cosa a riguardo di come il sistema lavorerà. Le strutture del run-time del sistema devono essere imposte più dal progettista che dal linguaggio. Le relazioni tra oggetti e i loro tipi devono essere progettate con grande cura perché esse determinano la bontà della struttura di run-time.


Progettare per cambiare

La chiave per massimizzare il riuso si trova nell’anticipare le nuove richieste e i cambiamenti alle richieste esistenti e nel progettare il sistema così che possa evolvere in armonia.


Per progettare il sistema così che sia robusto a tali cambiamenti, occorre considerare come il sistema potrebbe aver bisogno di mutare durante la sua vita. Un progetto che non tenga conto dei cambiamenti rischia maggiori riprogettazioni in futuro. Questi cambiamenti potrebbero portare alla ridefinizione delle classi e alla loro ri-implementazione, alla modificazione del client ed alla nuova esecuzione della fase di test. La riprogettazione influisce su molte parti del sistema software, ed i cambiamenti non anticipati sono invariabilmente costosi.
I design patterns aiutano ad evitare questo assicurando che un sistema possa cambiare in modi specifici. Ogni design pattern permette ad alcuni aspetti della struttura del sistema di variare indipendentemente dagli altri aspetti, rendendo in tal modo il sistema più robusto ad un particolare tipo di cambiamento.
Elenchiamo qui di seguito alcune delle cause più comuni di riprogettazione e i Pattern che consentono di affrontarle:


  1. Creazione di un oggetto specificando una classe esplicitamente.

Specificare un nome di una classe quando viene creato un oggetto vuol dire riferirsi ad una particolare implementazione invece che ad una particolare interfaccia. Questa operazione può complicare i cambiamenti futuri. Per evitare questo, occorre creare gli oggetti indirettamente.

Design Patterns: Abstract Factory, Factory Method, Prototype.




  1. Dipendenza da operazioni specifiche.

Quando si specifica una particolare operazione, ci si impegna con uno dei modi di soddisfare una richiesta. Evitando di fare richieste cablate nel codice, si può semplificare il cambiamento del modo con cui una richiesta viene soddisfattta sia a compile-time che a run-time.

Design Patterns: Chain of Responsibility, Command.




  1. Dipendenza dalle piattaforme hardware e software.

Le interfacce esterne dei sistemi operativi e le interfacce di programmazione delle applicazioni (API) sono differenti nelle diverse piattaforme hardware e software. Il software che dipende da una particolare piattaforma sarà più difficile da portare su altre piattaforme. Può anche essere difficoltoso da aggiornare sulla sua piattaforma nativa. È’ importante quindi progettare il sistema per limitare le sue dipendenze dalla piattaforma.

Design Patterns: Abstract Factory, Bridge.




  1. Dipendenza dalle rappresentazioni o dalle implementazioni dell’oggetto.

I client che sanno come un oggetto è rappresentato, memorizzato, allocato o implementato potrebbero necessitare di essere cambiati quando l’oggetto cambia. Nascondendo queste informazioni ai client si evita che il cambiamento avvenga in cascata.

Design Patterns: Abstract Factory, Bridge, Memento, Proxy.




  1. Dipendenze algoritmiche.

Gli algoritmi sono spesso estesi, ottimizzati e sostituiti durante lo sviluppo e il riuso. Gli oggetti che dipendono da un algoritmo dovranno cambiare quando l’algoritmo cambia. Quindi gli algoritmi che potrebbero cambiare devono essere isolati.

Design Patterns: Builder, Iterator, Strategy, Template Method, Visitor




  1. Stretto accoppiamento (tight coupling)

Le classi che sono strettamente accoppiate sono difficili da riusare da sole, dal momento che esse dipendono da ogni altra. Accoppiamento stretto porta a sistemi monolitici, dove non è possibile cambiare o rimuovere una classe senza capire e cambiare molte altre classi. Il sistema diventa una massa densa che è difficile da capire, portare e manutenere. Accoppiamento debole incrementa la probabilità che una classe possa essere riusata da sola e che un sistema possa essere capito, portato, modificato ed esteso più facilmente. I design pattern usano tecniche come un’accoppiamento astratto e stratificato per promuovere i sistemi accoppiati debolmente.

Design Patterns: Abstract Factory, Bridge, Chain of Responsibility, Command, Façade, Mediator, Observer.




  1. Estensione delle funzionalità “by subclassing”.

Personalizzare un oggetto mediante sottoclassi spesso non è semplice. Ogni nuova classe ha un’implementazione fissata precedentemente (inizializzazione, finalizzazione, eccetera). Definire una sottoclasse inoltre richiede una conoscenza profonda della classe parent. Per esempio sovrascrivere un’operazione potrebbe richiedere la sovrascrittura di un’altra. Ad un’operazione sovrascritta potrebbe essere richiesta la chiamata ad un’operazione ereditata. E il subclassing può portare ad un’esplosione delle classi poiché potrebbe essere necessario dover introdurre molte nuove sottoclassi anche solo per una semplice estensione.

La composizione di oggetti in generale e la delegation in particolare forniscono flessibili alternative all’ereditarietà per la combinazione di comportamenti. Le nuove funzionalità possono essere aggiunte a un’applicazione componendo gli oggetti esistenti in nuovi modi piuttosto che definendo nuove sottoclassi di classi esistenti. D’altra parte, l’uso massiccio della composizione di oggetti può rendere il progetto difficile da capire. Molti design patterns producono progetti in cui è possibile introdurre funzionalità personalizzate semplicemente definendo una sottoclasse e componendo le sue istanze con quelle esistenti.

Design Patterns: Bridge, Chain of Responsibility, Composite, Decorator, Observer, Strategy.


  1. Inabilità ad alterare le classi convenientemente.

Qualche volta occorre modificare una classe che non può essere modificata in modo conveniente. E’possibile che occorra usare il codice sorgente e che esso non sia disponibile (come nel caso delle librerie di classi commerciali). O potrebbe darsi che qualche cambiamento richieda la modifica di molte sottoclassi esistenti. I design pattern offrono modi per modificare le classi in queste circostanze.

Design Patterns: Adapter, Decorator, Visitor.


Questi esempi riflettono la flessibilità che i design patterns possiedono nell’aiutare a costruire software. Quanto sia cruciale tale flessibilità dipende dal tipo di software che si sta costruendo. Basta dare un’occhiata al ruolo dei design patterns nello sviluppo di tre classi ovvie di software: programma applicativo, toolkits e frameworks.


Programma applicativo

Se si sta costruendo un programma applicativo come un editor di documenti o un foglio elettronico allora il riuso interno, la manutenibilità interna e le estensioni interne sono altamente prioritarie. Il riuso interno assicura che non si debba progettare ed implementare niente di più di ciò che occorre. I design patterns che riducono le dipendenze possono incrementare il riuso interno. L’accoppiamento debole aumenta la probabilità che una classe di un oggetto possa cooperare con diverse altre. Per esempio, quando si eliminano le dipendenze su specifiche operazioni isolando e incapsulando ogni operazione, è possibile rendere più semplice il riuso di un’operazione in differenti contesti. La stessa cosa può accadere anche quando vengono rimosse le dipendenze algoritmiche e rappresentazionali.


I design patterns inoltre rendono un’applicazione più manutenibile quando essi sono usati per limitare le dipendenze dalla piattaforma e per stratificare un sistema. Essi aumentano l’estendibilità mostrando come estendere le gerarchie di classi e come sfruttare la composizione degli oggetti. La riduzione dell’accoppiamento favorisce l’estendibilità. L’estensione di una classe è più semplice se la classe non dipende da molte altre classi.


Toolkit

Spesso un’applicazione incorpora classi da una o più librerie di classi predefinite chiamate toolkit. Un toolkit è un insieme di classi correlate e riusabili progettate per fornire funzionalità utili e general-purpose. Un esempio di un toolkit è un insieme di classi per gestire liste, tabelle associative, pile e simili. La libreria “I/O stream” del C++ è un altro esempio. I toolkits non impongono un particolare progetto nell’applicazione: essi semplicemente forniscono funzionalità che possono aiutare l’applicazione a fare il suo lavoro. I toolkit lasciano che il progettista sia l’implementatore, evitando di registrare funzionalità comuni. I toolkits mettono in evidenza il riuso di codice. Essi sono l’equivalente object-oriented delle librerie di subroutine.


Il progetto di toolkit è più difficile che un progetto applicativo perché i toolkits devono lavorare in molte applicazioni per essere utili. In più lo scrittore del toolkit non è nella posizione di conoscere cosa saranno quelle applicazioni o i loro specifici bisogni. La cosa più importante è quella di evitare assunzioni e dipendenze che possono limitare la flessibilità del toolkit e conseguentemente la sua applicabilità e efficacia.


Frameworks

Un framework è un insieme di classi cooperanti tra di loro che rendono un progetto riusabile per una specifica classe di software. Per esempio, un framework può essere generato rivolgendolo verso la costruzione di editor grafici per differenti domini come disegno artistico, composizione di musica e CAD meccanico. Un altro framework può aiutare a costruire compilatori per diversi linguaggi di programmazione e macchine per produrre etichette. Un altro, ancora, può aiutare a costruire applicazioni di modelli finanziari. E’ possibile personalizzare un framework di una particolare applicazione creando sottoclassi specifiche di classi astratte del framework.


Il framework detta l’architettura della applicazione. Esso definirà la struttura globale, la sua suddivisione in classi e oggetti, la chiave responsabile a riguardo, come le classi e gli oggetti collaborano e il flusso di controllo. Un framework predefinisce questi parametri di progetto così che il progettista/implementatore dell’applicazione può concentrarsi sulle specifiche dell’applicazione. Il framework cattura le decisioni di progetto che sono comuni al dominio della sua applicazione. I framework dunque mettono in evidenza il riuso del progetto rispetto al riuso di codice, sebbene un framework di solito includa sottoclassi concrete che possono essere messe al lavoro immediatamente.
Il riuso a questo livello porta ad un’inversione del controllo tra l’applicazione ed il software sul quale essa è basata. Quando viene usato un toolkit (o una libreria di subroutine convenzionale per l’argomento trattato), viene scritto il corpo principale dell’applicazione e chiamato il codice che si vuole riusare. Quando viene usato un framework, viene riusato il corpo principale e scritto il codice che esso chiama. In tal caso occorrerà scrivere operazioni con nomi particolari e convenzioni di chiamata, ma questo riduce le decisioni di progetto che debbono essere prese.

Il risultato è che non solo è possibile costruire applicazioni più velocemente, ma le applicazioni hanno strutture simili. Esse sono più facili da mantenere e sembrano più consistenti a chi le utilizza. D’altra parte viene persa qualche libertà di creazione, dal momento che molte decisioni di progetto sono state fatte da altri al posto del progettista.


Se le applicazioni sono difficili da progettare e i toolkits sono più difficili, allora i frameworks sono ancora più difficili. Un progettista di framework rischia sul fatto che una architettura lavorerà per tutte le applicazioni nel dominio. Alcuni sostantivi cambiati nel progetto del framework riducono i suoi benefici considerevolmente, dal momento che il maggior contributo di un framework ad un’applicazione è l’architettura che definisce. Quindi l’imperativo per progettare il framework è quello di renderlo il più flessibile ed estensibile possibile.
Inoltre poiché le applicazioni sono così dipendenti dal framework per quanto riguarda la loro progettazione, esse sono particolarmente sensibili ai cambiamenti nelle interfacce dei framework. Mentre un framework evolve, le applicazioni devono evolvere con esso. Ciò rende debolmente accoppiate tutte le più importanti interfacce; altrimenti anche un lieve cambiamento del framework avrebbe pesanti ripercussioni.
Le problematiche del progetto già discusse sono le più critiche del progetto del framework. Un framework che indirizza la risoluzione di tali problematiche verso l’uso dei design patterns è più portato a raggiungere alti livelli di progetto e riuso di codice rispetto ad un framework che non si comporti in questo modo. I frameworks maturi di solito incorporano alcuni design pattern. Il modello fornito dai pattern aiuta a rendere l’architettura del framework adatta a molte applicazioni differenti senza necessità di riprogettazione.
Un vantaggio aggiuntivo si ha quando il framework è documentato con i design pattern che usa. Le persone che conoscono i pattern intuiscono il funzionamento del framework più velocemente. Anche le persone che non conoscono i pattern possono trarre beneficio dalla struttura che essi prestano alla documentazione dei frameworks. Aumentare la documentazione è importante per tutti i tipi di software, ma è particolarmente importante per i frameworks. I frameworks spesso fanno sono molto difficili da apprendere. Di solito occorre comprenderli a fondo prima di poter iniziare ad utilizzarli. I design pattern non rendono completamente comprensibili i frameworks, ma possono rendere meno difficoltoso l’apprendimento perché costruiscono gli elementi chiave del progetto di framework più espliciti.
Poiché i patterns e i frameworks hanno alcune similitudini, le persone spesso si meravigliano del fatto che essi differiscano e si stupiscono delle caratteristiche che li differenziano. Le tre principali differenze sono:



  1. I design pattern sono più astratti dei frameworks.

I frameworks possono essere incorporati nel codice, ma solo esempi di pattern possono essere incorporati nel codice. La forza dei frameworks sta nel fatto che essi possono essere scritti in linguaggi di programmazione e non solo studiati, ma eseguiti e riusati direttamente. Al contrario i design pattern devono essere implementati ogni volta che vengono utilizzati. Inoltre essi spiegano anche lo scopo che si prefiggono, i compromessi adottati e le conseguenze del progetto.


  1. I design pattern sono elementi di architettura più piccoli rispetto ai frameworks.

Un framework tipico contiene alcuni design patterns, ma il viceversa non è mai vero.


  1. I design pattern sono meno specializzati dei frameworks.

I framework hanno sempre un particolare dominio di applicazione. Un framework di un editor grafico potrebbe essere usato in una simulazione industriale, ma esso potrebbe non essere utilizzabile per una diversa simulazione. Al contrario i design pattern possono essere usati con qualunque tipo di applicazione. Design pattern più specializzati sono certamente possibili (ad esempio i design pattern per sistemi distribuiti o per programmazione concorrente), ma anche questi non dettano un’architettura di applicazione come fanno i frameworks.
I framework stanno diventando sempre più comuni e importanti. Essi sono i modi con cui i sistemi object-oriented ottengono il maggior riuso. Le applicazioni object-oriented più grandi finiranno per essere costituite da strati di frameworks che cooperano tra di loro. La maggior parte dei progetti e codici nell’applicazione proverrà (o sarà influenzata) dai frameworks che utilizza.

Come scegliere un design pattern

Esistono alcuni approcci differenti per trovare il design pattern corretto per il problema:




  • considerare come i design pattern risolvono i problemi di progetto;

  • esaminare gli scopi dei pattern;

  • studiare come i pattern sono in relazione tra loro;

  • studiare i pattern con scopi simili;

  • esaminare le cause di riprogettazione;

  • considerare cosa potrebbe variare nel progetto.


Come usare un design pattern

Una volta che si è scelto il design pattern, come si utilizza? Vediamo un approccio passo-passo per applicare effettivamente un design pattern.




  1. Leggere una volta tutto il progetto per avere una visione d’insieme.

Far particolare attenzione all’applicabilità ed alle conseguenze dell’applicazione per assicurarsi che il pattern sia realmente adatto al problema.


  1. Studiare la struttura del pattern, i partecipanti e le collaborazioni.

Assicurarsi di capire le classi e gli oggetti del pattern e come essi sono in relazione gli uni con gli altri.


  1. Pensare ad un esempio concreto di codifica del pattern.

Studiando il codice si comprende come implementare il pattern.


  1. Scegliere i nomi per i partecipanti significativi del pattern.

I nomi dei partecipanti nei design pattern sono solitamente troppo astratti per apparire direttamente in un’applicazione. Tuttavia è utile incorporare il nome del partecipante nel nome che appare nell’applicazione. Questo aiuta a rendere i pattern più espliciti nell’implementazione.


  1. Definire le classi.

Dichiarare le loro interfacce, stabilire le loro relazioni di ereditarietà e definire le variabili istanza che rappresentano i dati e gli oggetti di riferimento. Identificare le classi esistenti nell’applicazione su cui il pattern influisce e modificarle in accordo con esso.


  1. Definire nomi specifici di applicazione per le operazioni nel pattern.

I nomi generalmente dipendono dall’applicazione. E’ utile usare le responsabilità e collaborazioni associate ad ogni operazione come se fossero una guida. Inoltre occorre essere consistenti nella convenzione usata durante la scelta dei nomi.


  1. Implementare le operazioni per portare le responsabilità e le collaborazioni nel pattern.

Queste sono solo linee guida per ottenere quello che ci siamo prefissati. Ciascuno deve poi sviluppare un proprio modo di lavorare con i design pattern.


Abbiamo discusso su come usare i design pattern. Una trattazione più completa spenderebbe un po’ di parole anche su come non usarli. I design pattern non dovrebbero essere applicati indiscriminatamente. Spesso essi permettono di ottenere flessibilità e variabilità introducendo livelli addizionali di indirezione che però potrebbero complicare un progetto e/o costare in fase di esecuzione. Un design pattern dovrebbe essere applicato solo quando la flessibilità che offre è veramente necessaria. I risultati ottenuti in conseguenza dell’applicazione dei design patterns sono più utili quando vengono valutati sia i vantaggi che gli svantaggi del pattern.


Condividi con i tuoi amici:


©astratto.info 2017
invia messaggio

    Pagina principale