L’obiettivo
dell’Aspect Oriented Programming è quello di fornire metodi e tecniche per
decomporre i problemi in un certo numero di componenti funzionali e un certo
numero di aspetti, e comporre componenti ed aspetti per ottenere
l’implementazione del sistema.
Una
progettazione basata sui concetti della separation of concern necessita, come
abbiamo visto, di meccanismi che a livello di implementazione permettano di
ottenere questa separazione.
In generale
l’approccio AOP opera come mostrato in Figura14. Un linguaggio AOP fornisce
costrutti linguistici per separare sintatticamente componenti ed aspetti e
permette di integrare, tramite un meccanismo di unione detto weaver,
aspetti e codice funzionale per ottenere l’implementazione del sistema.
Figura
1 - L'idea base
dell'aspect oriented programming
Figura 2 -Implementazione intrecciata dello Stack
Dobbiamo
risolvere principalmente due questioni :
-
Quali sono
gli aspetti importanti da separare. Quello che stiamo cercando sono set di concern
riusabili da utilizzare nella decomposizione di problemi; alcuni di questi
saranno maggiormente “application specific”, ad esempio gli aspetti di
configurazione per servizi di rete, mentre altri saranno più generali, come la
sincronizzazione o il controllo di flusso. Ovviamente, differenti domini
richiederanno differenti set di concern. Alcuni di questi, sia specifici che
generali, risulteranno essere aspetti secondo la definizione data nel
capitolo precedente e sarà comunque importante, come già discusso, operarne la
scelta bilanciando tra complessità, ridondanza e localizzazione.
- Quali meccanismi di composizione possono essere usati. Se un aspetto non può essere separato in modo chiaro usando i consueti approcci come le chiamate a metodi e procedure (a cui ci si riferisce spesso come generalized procedures, caratterizzate dal fatto che sono chiamate dal codice client), abbiamo bisogno di altri meccanismi di composizione. Da questi meccanismi si vuole ottenere un vasto spettro di binding, oltre che un buon grado di adattabilità non invasiva, ad esempio attraverso composizioni o trasformazioni automatiche piuttosto che cambiamenti manuali.
Quando
necessario, per permettere una miglior esposizione dei dettagli implementativi,
useremo un semplice esempio di sincronizzazione di una struttura dati. Il
codice presentato in Figura15 è composto da statement che implementano le
funzionalità dello stack e statement (indicati con //sync) che implementano la sincronizzazione dei metodi.
Il concern primario e l’aspetto di sincronizzazione risultano fortemente
intrecciati.
Analizzeremo
quatto principali approcci che si propongono di soddisfare l’incapsulazione di
varie proprietà di sistema - inclusi gli aspetti che attraversano i moduli
delle unità funzionali - e forniscono concreti modelli di composizione. Queste
tecniche estendono il modello di
programmazione Object Oriented ma è importante osservare come alcune di queste
possano essere applicate anche in modelli procedurali [Highl+99].
Chiamate a
funzioni, parametrizzazioni statiche e dinamiche ed ereditarietà sono tutti
esempi di importanti meccanismi di composizione supportati dai linguaggi convenzionali.
Nonostante ciò, come già discusso in precedenza, non tutti gli aspetti
rilevanti possono essere adeguatamente composti usando tali meccanismi e nasce
la necessità di introdurne dei nuovi. Gli aspect
introdotti dall’Aspect Oriented Programming, i subject
(e relative regole) proposte nella Subject Composition, i filtraggi nel modello
d’oggetto della Composition Filters e le strategie di attraversamento del
Demeter sono alcuni esempi che discuteremo.
Idealmente
vorremmo ottenere un meccanismo di composizione di aspetti che permetta:
-
minimo accoppiamento tra aspetti e componenti
-
modalità e tempi di binding differenti tra aspetti
e componenti
-
adattabilità non invasiva degli aspetti verso
eventuale il codice esistente
Quello che si
richiede è minimo accoppiamento tra aspetti (e tra aspetti e componenti)
piuttosto che la completa separazione, perché la completa separazione è
possibile solo in alcuni specifici casi.
La ragione di
ciò è da ricercarsi nel grado di ortogonalità degli aspetti e nel fatto che
questi descrivono differenti prospettive sugli stessi singoli modelli.
Cerchiamo di chiarire questa osservazione attraverso un esempio.
Il disegno
presentato in Figura16-A mostra tre prospettive ortogonali di un oggetto
tridimensionale; in questo esempio le prospettive sono un trapezio e un
triangolo sui piani laterali e un cerchio sul piano di fondo. L’oggetto è
quindi scomponibile in prospettive. Pensiamo invece ora di usare i tre piani
come “generatori”, disegnando alcune figure bidimensionali su di essi e
cercando di interpretarli come prospettive di un oggetto tridimensionale da
costruire. Anche se queste risultano sono ortogonali (in termini di angoli), le
figure sui piani non possono essere scelte indipendentemente. La Figura16-B
mostra un set di figure bidimensionali che risultano inconsistenti per un
modello, con le quali cioè non è possibile costruire l’oggetto 3-D
considerandole come sue prospettive ortogonali, mentre in Figura16-C è
rappresentato un set consistente di figure bidimensionali.
Figura
3 - Esempio di
prospettive ortogonali consistenti e inconsistenti
Il minimo
accoppiamento è il principio su cui si basa l’introduzione di alcuni elementi
comuni in tutti gli approcci orientati agli aspetti: i cosiddetti join point.
In
linea di principio, i join point sono i punti in cui gli aspetti “influiscono”
sui componenti; vengono utilizzati in fase di decomposizione per mappare
(attraverso riferimenti) le corrispondenze aspetto-oggetto e dal meccanismo di
integrazione, il weaver, per fondere gli uni agli altri. Si può dire che la
natura stessa dei join point definisce i diversi tipi di approccio all’AOP.
Figura
4 - Join Point
E’ importante
che un meccanismo di composizione supporti una tecnologia adeguata di binding
temporale intendendo con ciò la possibilità di “legare” aspetti e componenti
prima o durante il runtime. Oltre a questo, l’obiettivo è quello di supportare
sia binding statici che dinamici. Queste caratteristiche sono profondamente
legate alla tecnologia che viene utilizzata del weaver e del compilatore e
incidono sul grado di adattabilità degli aspetti visto nel capitolo precedente.
Attraverso un
binding statico gli aspetti possono essere legati ai componenti e non possono
essere legati nuovamente; un esempio è il binding statico ed il metodo di
inlining nel C++. Con un binding dinamico un aspetto può essere automaticamente
integrato prima dell’uso e tolto dopo l’uso. Un binding di questo tipo può essere
utile in quei casi in cui si ha un frequente cambiamento di aspetti. L’usuale
implementazione, come quella in C++ o Java, fa uso delle virtual function
table.
Esistono, in
effetti, tipi di binding più sofisticati, come quelli parametrizzabili o quelli
sviluppati nella tecnologia HotSpot della Sun Microsystems, grazie ai quali è
possibile ottenere un meccanismo di composizione più generale con supporto
dinamico e statico allo stesso tempo.
Adattabilità non invasiva.
Con questo
termine si intende l’abilità di adattare un componente o un aspetto senza
modifiche manuali. Idealmente si vorrebbe esprimere ogni cambiamento come
un’operazione additiva, utilizzando qualche operatore di composizione per
aggiungere i cambiamenti al codice esistente. Il codice client non dovrebbe
essere fisicamente modificato ma le espressioni di composizione stesse
dovrebbero regolarne il cambiamento di semantica rispetto al codice originale.
Si potrebbe
pensare che l’ereditarietà stessa è un operatore di composizione: possiamo infatti
definire una nuova classe “per differenze”. Sfortunatamente l’ereditarietà è
non invasiva solo rispetto alla superclasse ed il codice client deve essere
cambiato in quelle situazioni in cui si vuole creare un oggetto della nuova
classe derivata. Quello che si cerca è un meccanismo non invasivo rispetto sia
ai componenti che al codice client, caratteristica che comunque non è possibile
ottenere in tutti i casi.
Nei prossimi
paragrafi vedremo esempi concreti di meccanismi di composizione che supportano questo
tipo di adattabilità non invasiva, come quello ottenuto dalle regole di
composizione e combinazione nella Subject Oriented Programming o quello
attraverso joinpoint e advice proposto nell’AspectJ. Un’ultima osservazione
riguarda il fatto che il basso accoppiamento è strettamente legato alle
caratteristiche di adattabilità. Se infatti l’accoppiamento tra gli aspetti è
minimo, risultano minimi anche i cambiamenti richiesti.
E' stata
proposta da Harrison e Ossher della IBM Thomas J. Watson Research Center come
estensione del paradigma object-oriented per risolvere il problema di gestire
diverse prospettive (subjective perspectives) applicate agli
oggetti da modellare [Clarke+99]. SOP rientra in quella che viene definita
Multi-Dimensional Separation of Concerns [Osshe+00], l’approccio IBM
all’aspect-oriented programming.
Consideriamo
ad esempio un oggetto che rappresenta un libro. Per il dipartimento marketing di una casa
editoriale sarà importante che includa attributi come area_di_appartenenza o breve_estratto, mentre il dipartimento di produzione sarà
interessato ad altri attributi come il tipo_di_carta o la rilegatura. La gestione in diversi
contesti di un oggetto non è l'unica ragione dell'introduzione di prospettive:
l'approccio cerca di risolvere il problema, discusso nel primo capitolo, di
integrare diversi sistemi, sviluppati con un alto grado di indipendenza, ad
esempio due applicazioni sviluppate per due differenti dipartimenti della casa
editoriale. Oltre a questo, l’obiettivo è rendere possibile l’integrazione di
nuove estensioni in un sistema esistente in modo non invasivo.
Ogni
prospettiva lavora su un cosiddetto subject. Un subject è una collezione
di classi o frammenti di classe (ad esempio i mixin[Flatt+98]) legati da una relazione di
ereditarietà o altre relazioni, come quella d’aggregazione, d’associazione e
così via. Quindi un subject è semplicemente un modello d’oggetto parziale o
completo.
I subject
possono essere composti attraverso particolari regole di composizione.
-
Regole di Corrispondenza : permettono di specificare
la corrispondenza tra classi, metodi ed attributi appartenenti a differenti subject che devono essere
composti.
-
Regole di Combinazione : permettono di specificare
come le classi, i metodi e gli
attributi dei subject sui quali sono state fissate regole di
corrispondenza, contribuiscono al risultato finale.
-
Regole di
Corrispondenza-Combinazione : permettono di specificare, allo stesso tempo,
corrispondenze e combinazioni.
Una
prospettiva è quindi un concern del sistema e i subject contribuiscono alla
rappresentazione del concern attraverso le composizioni e i filtraggi ottenuti
dalle regole. Ogni subject costituisce una nuova dimensione del processo di
design [Osshe+99], e tali dimensioni possono essere, in linea di
principio, ortogonali l’un l’altra.
Le regole di corrispondenza permettono di
specificare le corrispondenze, se ce ne sono, tra classi, metodi ed attributi
di oggetti che appartengono a diversi subject. Per esempio potremmo usare
una regola per esprimere che il
libro nell’applicazione del dipartimento di marketing è lo stesso libro
dell’applicazione per il dipartimento di produzione, anche se le corrispondenti
classi hanno differenti nomi. Le regole possono creare corrispondenze anche tra
metodi ed attributi di queste classi. Per esempio, potremmo dichiarare che
l’attributo titolo nella prima classe è lo
stesso attributo titolo_libro nella seconda. E’ inoltre
possibile usare regole di corrispondenza “automatiche” che permettano di
stabilire le corrispondenze delle due classi per tutti i membri che hanno gli
stessi nomi e adottare una tecnica di override per alcuni di essi, definendone
le specifiche regole di corrispondenza.
Dopo aver
stabilito le corrispondenze tra le due classi (libro), possiamo usare le regole di combinazione per specificare
come queste corrispondenze devono essere combinate. La classe risultante
includerà sia i metodi e gli attributi indipendenti, sia la combinazione dei
metodi e attributi “mappati” in accordo alla regola di combinazione. Per
esempio, un metodo di una classe potrebbe sovrapporsi al corrispondente metodo
dell’altra classe, oppure entrambi i metodi potrebbero essere eseguiti in uno
specifico ordine.
Quando
vengono composti due o più subject sviluppati in modo indipendente è necessario
solitamente sviluppare un subject addizionale, chiamato glue subject, che faccia da collante tra i
due e includa il codice necessario per combinarli.
Le regole di corrispondenza-combinazione sono
regole di convenienza e forniscono un metodo veloce per specificare, allo
stesso tempo, corrispondenze e combinazioni.
La IBM ha
sviluppato un prototipo di supporto ai subject (Hyper/J) che può essere
integrato nei tools quali VisualAge for C++, VisualAge for Java e Smalltalk.
Un’utile caratteristica nel prototipo per Java è la possibilità di specificare
le corrispondenze e le combinazioni in modo visuale attraverso un’interfaccia
grafica.
Utilizzando
l’approccio SOP possiamo separare in modo efficace la sincronizzazione e il
codice funzionale presentato in Figura15 perché entrambi possono essere
incapsulati in due separati subject. Gli elementi in gioco sono principalmente
quattro: l’implementazione puramente funzionale dello stack (Figura18),
l’aspetto di sincronizzazione (Figura20), un modulo di mappatura dei concern
(Figura19) e le regole di collaborazione e combinazione (Figura21).
Figura 5 - Implementazione funzionale della classe Stack
Figura
6 - Mappatura dei
concern
Figura
7 - Un esempio di
implementazione del concern sincronizzazione in SOP
Figura
8 - Un esempio di
hyper-module
La mappatura
dei concern permette di dichiarare in modo esplicito l’esatta corrispondenza
tra unità funzionali e concern di appartenenza: ad esempio, ogni operation pop appartiene alla dimensione Feature e precisamente al concern Kernel che implementa la sola funzionalità dello stack. Feature non è semplicemente un concern ma una dimensione.
Una delle caratteristiche di questo approccio, che rientra in quella che viene
definita Multi-Dimensional Separation of Concern, è infatti quella di gestire
più “livelli” di concern, chiamati dimensioni.
Una dimensione ha la caratteristica di poter contenere più concern e potremmo
pensarlo come un ulteriore meccanismo di aggregazione. Ad esempio potremmo
aggiungere una dichiarazione nella mappatura dei concern del tipo :
operation
stack.Stack.main : Development.Test
per indicare
che main appartiene alla dimensione Development riguardo al problema di testare la classe.
Il subject
che incapsula l’aspetto di sincronizzazione (Figuara20) assomiglia a tutti gli
effetti ad una normale classe. E’ necessario però notare che i metodi innerPop() e innerPush() sono vuoti e la loro
implementazione viene fornita attraverso la composizione.
A questo
punto abbiamo il subject con il codice funzionale, il subject con l’aspetto di
sincronizzazione e l’esatta mappatura unità-concern, quindi manca solo la definizione delle regole di
composizione e combinazione viste in precedenza.
Le regole
vengono raccolte in un hyper-module, che chiameremo HSyncStack (Figura21). In
questo modulo sono indicati i concern, le dimensioni e le regole che si
intendono usare. In particolare sarà necessario stabilire che durante la
composizione, il codice dell’unità Feature.Sync.innerPush dovrà essere sovrapposto dal
codice di Feature.Kernel.push e il codice di Futures.Sync.innerPop da quello di Feautur.Kernel.pop. Il modulo che viene generato
da questa fusione è una regolare classe java che potrà essere integrata
nell’applicazione.
La potenza di
questo meccanismo sta anche nell’abilità di specificare come deve essere
compiuta la composizione tra le unità. Osserviamo come nell’hyper-module siano
state definite le modalità di applicazione delle regole di composizione: sotto
la direttiva relationships, lo statement overrideByName indica una corrispondenza
“per nome” dei subject coinvolti mentre gli statement equate operation identificano gli eventuali
override. Queste sono solo alcune delle regole di composizione che si possono
adottare nell’Hyper/J e la gamma messa a disposizione permette di ottenere
diversi livelli di granularità.
La strategia maggiormente conosciuta è probabilmente
quella implementata dall’AspectJ, un’estensione per il linguaggio Java
progettata alla Xerox Palo Alto Research Center dal team di Gregor Kiczales
[Kiczale01].
I linguaggi
AOP hanno tre elementi critici : un modello dei join point, un meccanismo per
identificarli ed un meccanismo per realizzarne l’implementazione associata.
L’AspectJ è
basato su un ridotto ma potente set di costrutti:
-
I join point
sono punti ben definiti nel flusso d’esecuzione dell’applicazione.
-
I pointcut
permettono di rappresentare collezioni di join point oltre a particolari valori
che questi possono assumere.
-
Gli advice
sono particolari costrutti, tipo i metodi tradizionali, usati per definire i
comportamenti dei join point.
-
Gli aspect
sono unità di implementazione modulare dei crosscutting concern, composti da
pointcut, advice e le tradizionali dichiarazioni Java.
Il modello join point fornisce uno strumento
di riferimento con cui è possibile definire la struttura dei crosscutting concern.
Nel modello dell’AspectJ i join point possono essere considerati come nodi di
un grafo di chiamate (object call graph) , gestito a runtime. I nodi includono
i punti in cui un oggetto del sistema riceve una chiamata ad un metodo o punti
in cui viene fatto riferimento ad un attributo; gli archi rappresentano il
flusso di relazioni tra i nodi. Il controllo passa attraverso i join point due
volte: una prima volta quando viene attraversato, e una seconda volta dopo che
il comportamento associato al determinato join point è stato eseguito.
L’AspectJ
fornisce numerosi tipi di join point dinamici che permettono di ottenere
diversi livelli di granularità (Tabella1).
Tabella 1
Tipo
di join point |
Punti
nell’esecuzione del programma in cui…. |
methods calls constructor calls |
viene
chiamato un metodo o il costruttore di una classe. |
methods calls reception constructor calls reception |
un oggetto
riceve una chiamata ad un metodo o al costruttore. I join point reception
agiscono all’interno del codice chiamato, dopo l’istante in cui il controllo
è stato trasferito all’oggetto chiamato ma prima di qualsiasi chiamata a
metodi o costruttori. |
methods executions constructor executions |
un certo
metodo o costruttore è stato invocato. |
field gets |
un campo di
un oggetto, di una classe o interfaccia viene letto. |
field set |
un campo di
un oggetto o di una classe viene settato. |
exception handler executions |
un’exception
handler viene invocato. |
Un pointcut designator è un set di join point
che opzionalmente espone alcuni valori nel contesto di esecuzione dei join
point che contiene. Rappresenta a tutti gli effetti il meccanismo con cui i
pointcut possono essere definiti ed espressi. L’AspectJ fornisce un certo
numero di pointcut designator primitivi e permette di comporli per ottenerne
dei nuovi. Come esempio, consideriamo diagramma UML in Figura22, che
rappresenta un esempio di un semplice editor di figure.
Il pointcut
designator può essere pensato come strumento di “matching” a runtime di un cero
insieme di join point individuati all’interno dell’applicazione. Per esempio :
call (void
Point.setX(int)) || call(void Point.setY(int))
identifica
ogni chiamata al metodo setX o setY definiti in Point e, sinteticamente, questo codice consiste in due
pointcut designator call composti
attraverso un operatore “or”. La sintassi
di call
(come di tutti gli altri tipi primitivi di pointcut designator) è basata sulla
stessa sintassi dei metodi Java :
call (retun_type
object_type.method_name(arg_type, …))
Il
programmatore può comporre diversi tipi di pointcut designator (che possono far
riferimento a join point di diverse classi) ed etichettarli attraverso un nome.
In altre parole è possibile gestire il crosscutting tra le classi.
Ad esempio,
il seguente codice definisce un pointcut chiamato move che gestisce ogni chiamata a metodi che possono
contribuire al movimento dell’elemento Figure:
poincut move () : call(void
FigureEllement.moveBy(int, int)) || call(void
Point.setX(int)) || call(void
Point.setY(int)) || call(void
Line.setP1(Point)) || call(void
Line.setP2(Point)) ; |
Questo
pointcut designator è basato su un’esplicita enumerazione dei join point e
viene chiamato name-based poiché
strettamente vincolato al nome degli elementi che contiene.
Figura
9 - UML per il
semplice editor di figure
L’AspectJ
permette anche di definire in modo semplice quelli che vengono definiti
crosscutting property-based, con i quali è possibile
esprimere i pointcut in termini di proprietà dei metodi coinvolti più che
attraverso il loro nome. Questa caratteristica è ottenuta usando speciali
caratteri (wild cards) in all’interno della segnature del metodo.
Ad esempio il codice :
call
(* Point.*(…))
gestisce le
chiamate a qualsiasi metodo definito nella classe Point ( quindi getX(), getY(), setX(int), setY(int)); ancora, il pointcut :
call (* Point.get*())
fa
riferimento a qualunque metodo definito in Point il cui identificatore inizia con get e non accetta parametri.
Gli advice sono il meccanismo con cui è
possibile dichiarare il codice che deve essere eseguito all’occorrenza di certi
join point. In pratica, dopo che il programmatore ha definito i punti in cui
nell’applicazione deve essere applicato un particolare concern, quest’ultimo
può essere definito ed implementato attraverso l’advice. L’AspectJ ne supporta
diversi tipi tra cui before, after
e around. L’advice before viene
invocato nel momento in cui è raggiunto un join point o, in altre parole,
appena prima che il metodo identificato dal join point venga eseguito. L’after
advice, al contrario, viene invocato nel momento in cui il controllo ritorna
attraverso il join point, ossia appena dopo che il metodo è stato eseguito ma
prima che il controllo venga trasferito al chiamante. Gli advice around vengono
eseguiti dopo il before e prima dell’after. Qualunque sia l’advice, il suo
corpo può contenere qualsiasi codice che possa essere inserito in un consueto
metodo Java.
L’esempio che
segue rappresenta un semplice advice che stampa un messaggio se un elemento
Figura è stato mosso:
after() : moves() { <codice di stampa del messaggio> } |
Un aspect è l’unità modulare che l’AspectJ
fornisce per esprimere i crosscutting concern.
Gli aspetti sono definiti
attraverso un set di dichiarazioni in un modo del tutto analogo alle classi in
Java e possono includere dichiarazioni di pointcut, advice oltre ad ogni altro
tipo di dichiarazione permesso dal linguaggio. Attraverso gli aspect possiamo
implementare in modo del tutto modulare gli aspetti del sistema.
Ad esempio,
se volessimo costruire l’aspetto di monitoraggio per il movimento delle figure
potremmo implementare un aspect
di questo tipo :
aspect MoveTracking
{ pointcut
moves(): call(void FigureElement.slide(int, int)) || call(void Line.setP1(Point)) || call(void Line.setP2(Point)) || call(void Point.setX(int)) || call(void Point.setY(int)); before() : moves() { <codice di gestione> } after(): moves() { <codice
di gestione> } } |
Il meccanismo di composizione dell’AspectJ può essere utilizzato in modo efficace per la separazione del codice funzionale dall’aspetto di sincronizzazione presentati in precedenza. Lo stack verrà implementato come in Figura18, cioè attraverso una classe che ne gestisce la sola competenza funzionale mentre l’aspetto di sincronizzazione come nel listato di Figura23.
Figura 10 - Aspetto di sincronizzazione
L’approccio
Composition Filters [Bergma94] nasce dagli studi riguardo ai problemi di
inheritance anomalies nei sistemi concorrenti object-oriented, problemi che
abbiamo già accennato nel capitolo precedente.
Si basa su un
meccanismo a filtri di messaggio
(message filters) attraverso i quali devono passare i messaggi scambiati tra
gli oggetti. L’oggetto nel modello CF consiste in un kernel d’oggetto e in un layer
d’interfaccia, come mostrato in Figura24. Il kernel può essere
pensato come un nomale modello d’oggetto dei convenzionali linguaggi di
programmazione object oriented, come Java o C++. Il layer d’interfaccia
contiene un numero arbitrario di filtri di input ed output. I messaggi in
arrivo passano attraverso i filtri di input e i messaggi in uscita attraverso
quelli di output.
Figura
11 - Elementi di un
oggetto nel modello CF
I filtri
possono modificare il messaggio,
ad esempio cambiandone il nome o modificare l’oggetto di destinazione. Più
precisamente i filtri possono essere usati per redirigere i messaggi
(eventualmente modificati) verso due tipi di oggetti: verso oggetti interni, incapsulati nel layer
d’interfaccia, o verso oggetti esterni,
riferiti dal layer d’interfaccia. I filtri possono inoltre controllare,
scartare o bufferizzare messaggi, quindi l’azione è dettata unicamente dal tipo
di filtro usato.
Sono presenti
un certo numero di filtri predefiniti, per esempio i delegation filters (per
delegare i messaggi), i wait filters (per bufferizzare), gli error filters (per
le throwing exceptions) ed altri tipi di filtro possono essere creati ed
aggiunti. La modifica di un messaggio da parte di un filtro può dipendere dal
tipo di messaggio oltre che da alcune condizioni di stato (state conditions) definite attorno al
kernel dell’oggetto.
I filtri di
messaggio sono una potente tecnica che permette di implementare in modo ben
localizzato vincoli di sincronizzazione [Bergma94], vincoli real-time [Aksit+94], transazioni atomiche [Aksit+92], controllo d’errori
attraverso precondizioni [Bergma94] e molti altri importanti aspetti.
La
caratteristica di redirecting dei messaggi può inoltre essere usata per
implementare in modo efficiente delegazione ed ereditarietà
dinamica. La delegazione consiste nel redirigere alcuni messaggi
ricevuti dall’oggetto delegante verso un altro oggetto, il delegato,
mantenendone però il riferimento. In questo caso è necessario assicurarsi che,
quando l’oggetto delegato fa uso della keyword self,
si riferisca all’oggetto delegante e non a se stesso. In questo modo i metodi
degli oggetti delegati possono essere scritti come se fossero metodi del
delegante per cui gli oggetti delegati possono considerarsi a tutti gli effetti
come un’estensione, in modo simile alla relazione tra una classe e la sua
superclasse. Nel modello Composition Filters, delegare significa redirigere i
messaggi verso oggetti esterni, mentre ereditare significa redirigere i
messaggi verso gli oggetti interni .
Figura
12 - Delegation e
Inheritance nel modello CF
Un filtro può
inoltre delegare un certo messaggio a differenti
oggetti interni basandosi su alcune condizioni di stato. Questo significa che
la superclasse di un oggetto può cambiare a seconda del suo stato, permettendo
quella che viene definita ereditarietà dinamica.
Una generica operazione in un
programma object-oriented coinvolge spesso differenti classi che collaborano.
Ci sono normalmente due modi per implementare queste operazioni: mettere
l’intera operazione in un metodo di una delle classi oppure dividerla e
distribuirla all’interno di metodi di ognuna delle classi coinvolte.
Lo
svantaggio di quest’ultimo
approccio è che all’interno di ognuno dei metodi è necessario gestire troppe
informazioni relative alla struttura delle
classi (come le relazioni di tipo is-a e has-a). Una diretta
conseguenza è la difficoltà di adattamento dei cambiamenti nella struttura
delle classi e inoltre, la distribuzione delle operazioni su diverse classi,
rende complicato l’adattamento nel caso in cui l’operazione stessa cambi.
Per risolvere
questo conflitto, il progetto Demeter ha introdotto la nozione di adaptive method, anche conosciuto come
propagation pattern [Lieber96]. Un metodo adattivo non solo incapsula il
comportamento di una operazione, ma opera un’astrazione sopra la struttura
delle classi coinvolte. Per ottenere questo, il comportamento è espresso
attraverso una descrizione ad alto livello che indica :
-
come raggiungere i “partecipanti” dell’operazione,
cioè viene definita una strategia di attraversamento (traversal strategy)
-
cosa fare quando i partecipanti sono stati
raggiunti (adaptive visitors)
Il vantaggio
più grosso è che, grazie alle direttive ad alto livello, in entrambi i casi
viene indicato solo un minimo set di classi che partecipano all’operazione e le
informazioni riguardo le connessioni tra queste classi viene completamente
astratto. Questo meccanismo fa uso delle tecniche di reflection di Java in modo
che la struttura delle classi possano essere accedute a runtime.
Il
Demeter/Java, in particolare la libreria DJ, è un package Java che supporta
questo stile di programmazione, fornendo anche alcuni strumenti per
interpretare le strategie di attraversamento e gli adaptive visitor.
La Figura26
mostra un semplice metodo adattivo scritto in Java usando la libreria DJ. Lo
scopo del codice è quello di sommare i valori di tutti gli oggetti Salary
legati, attraverso una relazione has-a, all’oggetto Company.
import
edu.neu.css.demeter.dj.ClassGraph; import
edu.neu.css.demeter.dj.Visitor; class Company { static ClassGraph cg = new ClassGraph(); //class
structure Double sumSalaries() { String s = “from Company to
Salary”; //traversal strategy Visitor v = new Visitor() {
private double sum; public void start() { sum =
0.0; };
public void before (Salary host) {
Sum+=host.getValue(); }
public Object getReturnValue() {
Return new Double (sum); } }; return (Double) cg.traverse (this,
s, v) } // rest of Company definition } |
Figura
13 - Un semplice
adaptive methods
Il
funzionamento è il seguente:
la variabile
statica cg è un oggetto ClassGraph che rappresenta la struttura delle classi del
programma. Il ClassGraph è una semplice forma di
diagramma delle classi UML che descrive le relazioni has-a e is-a tra le
classi. La struttura della classe è gestita in ClassGraph attraverso la tecnica di reflection. Questa
struttura è usata dal metodo traverse che ne interpreta la strategia di
attraversamento. Il metodo inizia da un dato oggetto (in questo caso this ma potrebbe
iniziare da qualsiasi altro oggetto) e attraversa uno specifico percorso (“from Company to Salary”) eseguendo ogni metodo visitor applicabile lungo la strada.
In questo caso attraverso start viene inizializzata sum, poi, ad ogni passaggio per gli oggetti Salary viene effettuata la somma e, prima di finire,
viene costruito il valore di ritorno.
La separazione della strategia di
attraversamento (che specifica dove andare), l’adaptive visitor (cosa fare) e
il class graph (il contesto in cui valutare) permettno a questo meccanismo di
essere riusato senza cambiamenti in un set di programmi. I dettagli, come il
numero di classi tra Company e Salary, non sono importanti e risultano nascosti
grazie al meccanismo di attraversamento: potrebbe infatti trattarsi di una
applicazione con una grossa organizzazione di strutture (divisioni,
dipartimenti, work group ecc..) con tante classi, oppure un’organizzazione con
una piccola struttura.
Il termine Adaptive Programming
fu introdotto alcuni anni fa da Lieberher [Lieber96] per consentire la gestione
di un certo insieme di aspetti riguardanti la struttura delle classi.
Successivamente fu esteso alla gestione di aspetti di sincronizzazione e remote
invocation [Lieber98]. In accordo ad una definizione più recente, l’A.P. è un
caso “particolare” dell’Aspect Oriented Programming dove alcuni blocchi (come i
componenti e gli oggetti) sono espressi in termini di grafi ed altri (i
crosscutting concern) fanno riferimento al grafo attraverso una strategia di
attraversamento. Si può dire che l’A.P. è un sott’insieme dell’AOP in termini
di grafi e strategia di attraversamento .
Copyright (C)2002 Fabrizio Rovelli. La copia letterale e la distribuzione di questa pagina nella sua integrità sono permesse con qualsiasi mezzo, a condizione che questa nota sia riprodotta.