Prima di introdurre le tecniche
proposte dall’Aspect Oriented è necessario descrivere in modo dettagliato cosa
si intende per aspetti e inquadrare in modo più preciso quelli che vengono
definiti cross-cutting concern.
Lo sviluppo di un sistema
software può essere visto da un utente come un dominio tecnico su cui lavorare,
ma per uno sviluppatore è visto come un dominio funzionale che coinvolge, in
modo naturale, diversi domini tecnici trasversali, come sistemi operativi, basi
di dati, reti e così via. A loro volta tali domini possono essere considerati
domini funzionali di altri sottodomini tecnici come la sicurezza, la
persistenza o i protocolli.
Poiché questi
domini tecnici possono essere pensati e discussi indipendentemente, sembra
abbastanza naturale mantenerli separati a livello di specifica e di sviluppo,
in modo tale che ai programmatori non sia necessaria la competenza di tutti i domini coinvolti. E’
interessante notare come, con questo tipo di approccio, ogni individuo
coinvolto nello sviluppo di un sistema software, possa concentrarsi con maggior
dettaglio sugli aspetti di sua competenza: ogni sviluppatore può offrire la sua
esperienza in merito a specifici obiettivi di sviluppo.
La situazione
è quella di avere un sistema che deve soddisfare una determinata competenza
primaria (o concern primario), attorno al quale è possibile identificare
aspetti o concern secondari che soddisfano proprietà del concern primario o
dell’intero sistema (Figura7).
La particolarità
di alcuni di questi concern, che
possono ad esempio riguardare la sincronizzazione, la persistenza, il profiling, il logging o la gestione degli errori, è quella di non essere
direttamente esprimibili in modo esplicito attraverso alcun meccanismo di incapsulazione
degli attuali linguaggi di programmazione OO. Come conseguenza le competenze
vengono distribuite all’interno della struttura del concern primario, i cui
moduli diventano, a livello di codice, un “mix” di concern differenti
(Figura8).
Figura 1 – Concern primari e secondari
Figura
2 – Concern
intrecciati nei moduli funzionali
Per
realizzare la separazione delle competenze fino alla fase di programmazione,
sono già stati introdotti approcci e infrastrutture software (framework).
Come già
discusso, le tecniche più popolari ed efficaci per realizzare questo obiettivo
sono state quelle object-oriented e quelle component-based che permettono di
realizzare astrazioni ad alto livello per un dato dominio, in modo che possano
essere facilmente riutilizzate ed integrate all’interno di un programma.
Tuttavia queste tecniche devono affrontare alcuni difficili problemi.
Un problema
noto come “inheritance anomalies”
[Matsu+93] è emerso dalle discussioni tra ricercatori del settore sui conflitti
tra sincronizzazione e riuso, e sulla necessità di una migliore separazione tra codice di
sincronizzazione e funzionalità primarie in sistemi concorrenti. Più precisamente,
i vincoli sulla concorrenza comportano spesso la ridefinizione di numerosi metodi nelle sottoclassi,
riducendo in questo modo l'efficacia in termini di riuso dei componenti che
l'ereditarietà dovrebbe favorire. Tutti i tipi di inheritance anomaly, sia
relativi alla programmazione ad oggetti concorrente che relativi a sistemi
real-time ad oggetti, comportano una ridefinizione forzata della maggior parte
delle operazioni ereditate nelle classi derivate e questo vanifica gli sforzi
spesi nel progetto di una gerarchia d'ereditarietà, non permettendo uno
sviluppo incrementale.
La
generalizzazione di queste problematiche ha portato alla luce la necessità di
provvedere a problemi più generali che, in prima istanza, nascono dal tentativo
di effettuare un'integrazione di proprietà che seguono differenti regole di
composizione. Tali problemi, come già accennato, prendono in considerazione il
fatto che molti concern non-funzionali di un sistema non riescono ad essere
fisicamente separati rispetto ai moduli che si occupano delle caratteristiche
primarie, ma devono essere distribuiti e sparsi all’interno della loro
struttura perdendone ogni caratteristica di modularità.
Potremmo
quindi definire un aspetto nei
seguenti termini: a livello di design, rappresenta un crosscutting concern,
quindi una competenza che taglia e attraversa la struttura del concern primario
e di altri concern; a livello di implementazione, un appropriato costrutto
linguistico di programmazione che permetta a questi concern di essere catturati
ed espressi in unità modulari. In generale quindi un aspetto è un crosscutting
concern ben modularizzato.
E’ importante
a questo punto proporre una classificazione degli aspetti [Constan02]. Da un
lato, un possibile modo è quello di classificare gli aspetti dal punto di vista
del sistema: sotto quest’ottica è
possibile distinguere tra aspetti funzionali e non funzionali e classificarli
rispetto al loro grado di attraversamento all’interno della gerarchia delle
classi o tra i livelli di un’architettura. Da un punto di vista degli aspetti
stessi e delle loro reciproche interazioni, è possibile invece distinguere tra
aspetti statici e dinamici, o caratterizzare alcune loro importanti proprietà
come il grado di trasparenza, l’ortogonalità e il supporto al design by contract.
Come abbiamo
visto, gli aspetti sono competenze dipendenti dal dominio del problema. Come
tali, possono soddisfare requisiti funzionali e non-funzionali. I requisiti
funzionali gestiscono il comportamento del sistema in termini di servizi,
mentre i requisiti non-funzionali forniscono proprietà e vincoli a questi
servizi, interessandosi delle performance e delle semantiche del sistema.
Esempi di requisiti non funzionali sono la sincronizzazione, lo scheduling,
l’autenticazione, il tracing e il logging. L’interesse dell’Aspect Oriented
Programming è rivolto in particolare agli aspetti non-funzionali.
In un sistema object oriented,
gli aspetti possono essere caratterizzati rispetto al loro livello di
crosscutting sulla gerarchia di classi del sistema. Questo approccio permette
di identificare aspetti intra-class (o intra-object) e inter-class ( o
inter-object).
Naturalmente le due categorie non
sono mutuamente esclusive, e un aspetto può attraversare diversi metodi di una
classe ma allo stesso tempo può attraversare differenti oggetti. Come vedremo,
l’AspectJ, il modello Composition Filters e il DemeterJ (Adaptive Programming)
sono in grado di fornire una definizione di aspetto che permette di gestire
entrambi i livelli di crosscutting, inter-object e intra-object.
Aspetti tra layer.
Per quanto
riguarda lo sviluppo di grandi sistemi software a più livelli, un’altra
distinzione è quella tra aspetti che possono esistere a livello di sistema e
aspetti a livello di applicazione poiché alcuni di essi possono attraversare
questi diversi layer [Lodew00].
Esempi di
aspetti multi-layer sono la Quality of Service [IWQoS02], che può attraversare
l’application layer , l’operating
system layer e il network layer, o l’aspetto crittografica che applica
differenti algoritmi in ogni livello.
Può
essere fatta un’ulteriore classificazione considerando due particolari
dinamiche :
-
l’associazione a run-time tra aspetti e componenti
-
i cambiamenti dinamici delle politiche di gestione
degli aspetti.
Nel
primo caso un aspetto A1 può attraversare i componenti C1
e C2 al tempo T1 ma potrebbe attraversare i componenti C1,
C2 e C3 al tempo T2. Questo comporta che il
“livello” di cross-cutting viene esteso a run-time. Nel secondo caso un aspetto
A1 può tagliare i componenti C1 e C2 al tempo T1
adottando una politica di gestione P1 e, al tempo T2, la
sua politica può cambiare in P2. In questo caso, il cambiamento di
politica può richiedere un’adattabilità run-time dell’aspetto, ad esempio la
sostituzione di un riferimento di un aspetto con un altro. Ovviamente possiamo
avere combinazioni dei due casi.
Un
aspetto introdotto a run-time e un
aspetto la cui politica deve cambiare a run-time non appartengono
necessariamente alla stessa categoria. L’introduzione di aspetti a run-time
(vedi grado di trasparenza) prevede la necessità di sistemi aperti [Kiczales97]
che possano gestirne le dinamiche di inserimento . Se un aspetto è invece
progettato in modo da soddisfare un certo comportamento o fornire servizi che
possono essere sensibili allo stato (di sistema o di un altro aspetto) è una
preoccupazione dell’aspetto stesso e del suo comportamento.
Figura
3 – Aspetti statici
e dinamici
A
seconda che la politica di un aspetto debba adattarsi o meno a run-time possiamo introdurre le notazioni di
aspetti dinamici o statici.
Un
aspetto come l’autenticazione può essere considerato statico poiché è improbabile
che la sua politica di autenticazione cambi durante l’esecuzione del programma;
d’altra parte una politica di scheduling in sistemi real-time potrebbe più
probabilmente adattarsi a run-time. E’ per questo che possiamo considerare ad
esempio la schedulazione come un aspetto dinamico.
Può essere
interessante analizzare il livello con il quale un aspetto è “invasivo” verso
gli altri componenti o gli altri aspetti del sistema, cioè il grado di modifica
necessaria al codice client per introdurre un aspetto o cambiarne la politica.
Idealmente un sistema dovrebbe supportare adattabilità statiche e dinamiche non
invasive (plug-compatibility) e il codice client non dovrebbe preoccuparsi
dell’introduzione di aspetti nel sistema [Filman00]. Una tale caratteristica,
alla base anche delle architetture component-based, permette di introdurre
aspetti nell’applicazione senza alterare il codice esistente permettendo alti
gradi di riuso e minime modifiche al sistema. Per ottenere proprietà statiche
di adattabilità, devono essere forniti meccanismi a livello di linguaggio (come
i joinpoint, e gli advice nell’AspectJ) mentre l’adattabilità dinamica può
essere gestita da meccanismi che osservino la semantica corrente del sistema,
come i contratti.
Possiamo
osservare gli aspetti anche rispetto al grado con cui supportano il Design by
Contract [Meyer92]. Secondo questo principio, un sistema software è visto come
un set di componenti comunicanti la cui interazione è basata su precisi mutui
obblighi (contratti). La possibilità di sviluppare attraverso tale principio in
un contesto di AOP è stato affrontato in [Klaer+00]. Il DBC è stato introdotto nel linguaggio di programmazione Eiffel dove il contratto è
profondamente legato nella definizione della classe e viene ereditato. Separare
i contratti dai componenti funzionali permette di raggiungere alti livelli di
riusabilità e adattabilità e nel caso di comportamenti concorrenti permette di
eliminare il problema delle inheritance anomalies [Aksit94]. Attualmente,
l’implementazione dell’AspectJ permette un supporto per la definizione di
contratti come aspetti attraverso l’implementazione di pre e post
condizioni.
L’uso dei
contratti nell’AOSD permette, come accennato prima (gradi di trasparenza), di
implementare un possibile meccanismo di adattabilità dinamica degli aspetti. Le
definizioni dei contratti possono essere controllate a compile-time o a
run-time per verificare che la composizione in atto sia valida e corretta in accordo
al set di contratti forniti dallo sviluppatore. Un tale meccanismo risulta di
particolare importanza in architetture che vogliono supportare l’adattabilità a
run-time. La ragione è attribuibile al fatto che l’adattabilità di un aspetto
può implicare alterazioni del flusso dati e di esecuzione del programma base,
generando possibili sovrapposizioni di priorità (scorretto ordine di
attivazione) di un aspetto rispetto ad un altro.
Poiché un
aspetto può essere creato per un determinato programma ma può potenzialmente
essere usato da altri programmi, è necessario controllare se l’aspetto è valido
e corretto rispetto al contesto di un dato programma base. La possibilità che un aspetto possa
verificare autonomamente la sua correttezza rispetto al sistema nel quale è
stato agganciato permette di ottenere codice più robusto oltre che una migliore
integrazione con il sistema base. A tal fine è necessario un meccanismo di
verifica dei contratti a run-time che controlli inoltre i contratti degli
aspetti che dinamicamente vengono aggiunti al sistema [Findl+01].
E’ altamente
improbabile che gli aspetti siano completamente indipendenti gli uni rispetto agli altri. Molti esempi
concreti nelle progettazioni reali coinvolgono aspetti che tendono ad essere
interdipendenti. Tale situazione è definita come non-ortogonalità tra aspetti
ed è importante perché coinvolge l’intera semantica del sistema. In stretta
relazione a questo c’è la cosiddetta problematica dell’ordine di attivazione già introdotta nella discussione
sul design by contract.
Consideriamo
ad esempio un sistema concorrente che supporta il protocollo readers-writers.
La sincronizzazione deve essere attivata e verificata prima della schedulazione
e una possibile inversione di tale ordine potrebbe violare le semantiche del
sistema. Se dovesse poi essere introdotto un ulteriore aspetto di
autenticazione, questo dovrebbe a sua volta essere gestito prima della
sincronizzazione.
Ci sono
proposte concrete sia a livello di framework, attraverso moderatori di aspetti
basati su pattern [Consta99], sia a livello di linguaggio, attraverso
l’introduzione di meccanismi adeguati per la gestione delle attivazioni
(AspectJ e Composition Filters supportano tale tecnologia).
Nel primo
capitolo è stato evidenziato come esista una profonda difficoltà a mappare gli
aspetti progettati a livello concettuale sul piano dell’effettiva
implementazione.
Se gli
aspetti funzionali riescono molto spesso ad essere ottimamente espressi
mediante le consuete decomposizioni e metodologie OO, non si può dire lo stesso
di quelli non funzionali il cui codice risulta distribuito, intrecciato e
ridondante tra i moduli funzionali.
In Figura10 è
riportato come esempio [Kiczal+98] il risultato grafico di un’analisi condotta
sul progetto software Jakarta Tomcat dell’Apache Software Foundation. Le linee
rosse rappresentano il codice che gestisce il logging dell’applicazione. A
differenza di altre funzionalità come il parsing XML o l’URL pattern matching,
il logging risulta estremamente non modularizzato e il codice distribuito
all’interno dell’applicazione.
Figura
4 – Il logging
risulta distribuito all’interno dell’applicazione
I differenti
approcci introdotti nel campo dell’Aspect Oriented Software Development che
vedremo nel prossimo capitolo, dichiarano di essere possibili soluzioni a
questo problema.
Vediamo ora
di descrivere brevemente alcune situazioni in cui è presente il problema dei
crosscutting concern per metterne in luce le problematiche di localizzazione
del codice. Se l’esposizione precedente ha permesso di classificare diversi
tipi di aspetti a livello di design, la seguente trattazione mostrerà le
difficoltà di operarne una buona separazione a livello d’implementazione.
Consideriamo
il codice intrecciato presentato in Figura11, risultato dell’applicazione di un
Pattern Observer [Hanen02].
Figura
5 - Implementazione
del Pattern Observer
-
Entrambe le classi Point e GuiElement contengono metodi ridondanti per collegare e
scollegare gli observer oltre che la stessa definizione dell’istanza observer. L’intenzione di queste definizioni è la stessa e
per questo è possibile obiettare che entrambe le classi contengono codice ridondante
e intrecciato. E’ importante notare come tale codice non sia di effettiva
competenza delle classi ma degli observer coinvolti. Siamo cioè in presenza di un intreccio di codice
oltre che una sovrapposizione di competenze.
L’object-oriented propone una possibile soluzione
a questo problema poiché le due classi possono avere la stessa superclasse
contenente le informazioni relative all’implementazione all’observer. Una
tecnica come il refactoring con estrazione
di superclassi [Fowle+99] sembra quindi adatta ad ottenere i risultati
voluti, ma ci sono tuttavia diverse ragioni per non affrontare il problema
attraverso l’ereditarietà. Nella programmazione orientata agli oggetti,
l’ereditarietà esprime una relazione di sottotipi (subtype) più che un meccanismo
di riuso. Questo tipo di relazione non è quella desiderata perché considera ed
impone concettualmente una radice comune di entrambe le classi. D’altra parte
estrarre una superclasse in un linguaggio di programmazione che supporta solo
l’ereditarietà singola non è fattibile, per cui si rende necessaria
un’organizzazione ad eredità multipla con tutte le problematiche che può
comportare [Meyer97].
-
Lo statement this.informObserver() si presenta tre volte in
altrettanti metodi della classe Point ed una volta nella classe GuiElement. Lo scopo dello statement è quello di informare
tutti gli observer in ascolto di ogni cambiamento di stato dell’oggetto. In
particolare, lo stato di un’istanza di Point è descritto attraverso le coordinate x ed y
mentre lo stato di GuiElement è descritto attraverso color. Questi statement intrecciano il codice dei
metodi della classe introducendo ridondanza. Per questo tipo di codice
intrecciato a granularità più fine
non c’è una soluzione object-oriented poiché non si tratta di metodi
intrecciati nell’oggetto ma di statement dell’observer all’interno dei metodi
di Point.
-
Gli assegnamenti this.x = x e this.y
= y
esistono nel metodo setXY oltre che nei metodi setX e setY della classe Point. Da un certo punto di vista questo tipo di codice
intrecciato sembra essere più complesso dei precedenti. L’observer dovrebbe
essere informato esattamente quando viene cambiato lo stato del punto; il
metodo setXY non può per questo invocare setX e setY perché l’observer verrebbe
informato due volte a causa del this.informObserver necessario in tutti e tre i
metodi. In questa situazione, la necessità di inserire statements di competenza
dell’observer, indebolisce la modularità dei metodi stessi all’interno della
classe e il codice che risulta ridondante è quello della classe stessa oltre
che dell’observer.
Un altro
esempio spesso discusso di competenze intrecciate è quello relativo al codice
d’implementazione del pattern Singleton [GoF95], presentato in Figura12.
Figura
6 - Implementazione
del Pattern Singleton
Da una parte
il corpo di entrambi i metodi getInstance è pressoché lo stesso, per
cui si potrebbe considerare il corpo dei metodi come codice intrecciato.
D’altra parte le dichiarazioni di getInstance differiscono solo nei tipi
che restituiscono. E’ quindi più appropriato considerare l’intero metodo come
esempio di codice intrecciato. Linguaggi di programmazione come Java che non
permettono di dichiarare né metodi covarianti né tipi generici, non forniscono alcuna soluzione
appropriata a questo tipo di codice intrecciato.
Anche in
questo caso abbiamo un aspetto, la
“singola istanziazione”, che non riesce ad essere separato e localmente
incapsulato, ma viene distribuito all’interno dei moduli funzionali di ogni singleton, inserendo ridondanza (si
osservi anche come la definizione di instance differisca solo per il tipo di dato e come quindi
risulti ridondante nelle due classi). Un migliore meccanismo di decomposizione
dovrebbe permettere la creazione di un modulo (l’aspetto “singleton”) in grado
di verificare la singola istanza di tutte le classi coinvolte.
Figura
7 – Aspetto
Observer e Aspetto Singleton
Gli esempi
presentati permettono di identificare differenti tipi di codice intrecciato;
alcuni di questi possono essere ricondotti all’esposizione fatta all’inizio del
capitolo, quando è stata presentata una classificazione, più ad alto livello,
degli aspetti. Se nella prima classificazione l’obiettivo era quello di esporre
le diverse modalità con cui un concern poteva presentarsi a livello di
progettazione e design del sistema software, ora l’attenzione è rivolta ai
differenti tipi di codice intrecciato (tangled-code) che si possono presentare
in fase di implementazione.
Crosscutting code dipendenti e indipendenti dall’unità modulare.
Questa caratteristica descrive se
il codice dipende o meno dal modulo funzionale a cui è intrecciato o dalla
“posizione” dove il codice viene inserito.
Questo tipo
di tangled-code dipende, in pratica, dal tipo di unità che attraversa. Per
esempio gli statement this.informObservers(), che possiamo trovare in
entrambe le classi del pattern observer, sono esempi di codice intrecciato dipendente dall’oggetto, perché dipende
dalla variabile this e presuppone che l’oggetto
riferito attraverso this contenga un metodo informObserver(). Esistono anche tangled-code dipendenti dal metodo, che dipendono cioè
da informazioni relative al metodo che intrecciano, come parametri o variabili
locali. E’ da notare che, anche se il codice è dipendente dall’oggetto, questo
non significa che “agisca” unicamente sullo stesso oggetto: this in GuiElement si riferisce a differenti
oggetti rispetto a this in Point.
Una visione
più ad alto livello, considerando cioè il tangled-code come parte di un aspetto
“observer”, ci porta a quelli che sono stati definiti aspetti inter-class ed
intra-class.
I metodi per
collegare e scollegare gli observer (attachObserver(…) e detachObservers(…)) sono esattamente gli stessi
nelle due classi coinvolte cioè il loro codice non presenta “variazioni” e per
questo possono essere definiti costanti.
Anche gli statement this.informObservers() non cambiano (anche se this, come già detto, è riferito a differenti oggetti)
e possono considerarsi un altro esempio di crosscutting code costante. Al
contrario, l’implementazione del pattern Singleton contiene tangled-code variabile : il tipo di ritorno del metodo getInstance cambia a seconda del contesto in cui è inserito.
Anche il corpo di questi metodi è variabile perché nel SingletonA viene creata una nuova istanza al SingletonA e nel SingletonB una nuova istanza al SingletonB.
Quando si
parla di crosscutting costante si
intende quindi che la modalità con cui certi codici tagliano una certa
struttura è completamente descritta senza alcuna alterazione. In altre parole
si intende che la strategia del codice non necessita di modifiche e può essere
usata così com’è in nuovi e
diversi contesti. Un crosscutting variabile,
pur richiedendo tecniche di composizioni più complesse, ha sicuramente il
vantaggio di ottenere maggior adattabilità, poiché basa la sua strategia su
parametri che dipendono da dove viene inserito (ad esempio le classi che
taglia) e permette una maggior integrazione in contesti diversi.
Anche in
questo caso l’AspectJ, come altri approcci che vedremo, ne permette un’efficace
gestione di entrambi.
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.