3. Aspect-Oriented Programming

 

 

 

 

 

 

 

 

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.

 

 

Decomposizione e composizione Aspect-Oriented

 

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

 

Minimo accoppiamento.

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

Binding.

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.

 

 

Subject-Oriented Programming – Hyper/J

 

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à.

 

 

Aspect Oriented Programming – AspectJ

 

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.
I join point call sono nell’oggetto chiamante o in nessun oggetto se la chiamata è da un metodo statico.

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

Composition Filters

 

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.

 

 

Adaptive Programming / Demeter

 

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.