2. Aspetti e crosscutting concerns

 

 

 

 

 

 

 

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.

 

 

 

Classificazione e caratteristiche degli aspetti

 

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.

 

Aspetti funzionali e non-funzionali

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.

 

Aspetti inter-class e intra-class.

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.

 

Aspetti statici e dinamici.

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.

 

Grado di trasparenza

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.

 

Supporto al Design by Contract

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

 

Non-ortogonalità

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

 

 

Intreccio del codice a livello d’implementazione

 

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.

 

Crosscutting code costanti e variabili

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.