I. Introduction▲
Depuis une trentaine d'années, l'approche orientée objet a procuré des bénéfices indénombrables dans la communauté génie logiciel. En effet, elle permet de modulariser de larges systèmes d'une manière intuitive, car elle offre un modèle de programmation riche très proche du monde réel. Aujourd'hui, l'orienté objet bénéficie d'un excellent background notamment grâce à la popularisation d'UML. En plus, une multitude de langages de programmation (Java, C++, Smalltalk…) ont adopté ce paradigme de manière native.
Cependant, l'approche orientée objet montre ses limites et échoue face à la modularisation des préoccupations transversales au système. Parmi les préoccupations transversales plus courantes, on trouve la sécurité, la gestion transactionnelle de la persistance, la synchronisation, le logging (Figure). Ces préoccupations ont la particularité d'un côté, d'être dispersées à travers plusieurs modules, et d'un autre côté, d'être enchevêtrées avec les modules métier du système. Ces deux phénomènes dégradent considérablement le maintien, la compréhension et l'évolutivité du code.
C'est là que la programmation orientée aspect intervient en apportant des mécanismes à la fois simples à appréhender et puissants qui permettent de capturer des préoccupations transversales. En effet, l'orientée aspect procure une solution élégante aux problèmes d'enchevêtrement et d'éparpillement du code. Aujourd'hui, cette technique d'ingénierie logicielle s'affirme comme étant la prochaine étape pour le découpage des systèmes en offrant une nouvelle dimension pour la modularisation notamment avec la notion d'aspect. En effet, parallèlement aux classes qui sont un support idéal pour modulariser les préoccupations métier du système, les aspects sont un support pour capturer les préoccupations transversales. Dans une démarche orientée aspect, les préoccupations transversales peuvent évoluer indépendamment des préoccupations métier et vice-versa (Figure). Et afin, que l'application finale prenne en compte toutes les préoccupations, le système passe par une étape dite de tissage d'aspects. Durant cette étape, les préoccupations transversales encapsulées dans les aspects vont être tissées ou intégrées dans les préoccupations métier.
Actuellement, une panoplie de langages et de plateformes orientées aspects existe (AspectJ, AspectWerkz, AspectC++…). Dans cet article, nous allons nous intéresser au langage AspectJ qui est à notre connaissance l'expérimentation la plus aboutie de langages orientés aspects. Ce n'est pas un langage à part entière, mais une extension du langage Java qui permet à ce dernier de supporter la programmation orientée aspect.
Dans cet article, nous allons tout d'abord nous intéresser dans la section, aux concepts de la programmation orientée aspect à savoir les points de jonction, les coupes et les codes advices. La présentation de ces concepts sera indépendante de toute implémentation afin de faciliter leur appréhension. Puis, nous nous intéresserons dans la section au langage AspectJ en présentant l'implémentation des principaux concepts de l'AOP à travers ce langage. Et enfin, nous terminerons dans la section par un exemple pratique d'une application orientée aspect. Mais avant, nous allons voir les problèmes engendrés par la non-modularisation des préoccupations transversales ainsi que la méthodologie générale d'une démarche orientée aspect.
I-A. Préoccupations transversales▲
Les préoccupations transversales (en anglais crosscutting concerns) sont les fonctionnalités dites non métier. Un développeur est souvent confronté à ce genre de fonctionnalités lorsqu'il développe une application de grande envergure. Dans ce cas, même si on applique une bonne modularisation verticale des préoccupations métier, on aura toujours un problème de préoccupations horizontales qui transversent l'ensemble des modules métier. Il existe en fait deux principaux symptômes liés aux préoccupations transversales : l'enchevêtrement du code et l'éparpillement du code
I-A-1. Enchevêtrement du code (code tangling)▲
L'enchevêtrement du code est provoqué quand un module est implémenté pour traiter plusieurs préoccupations en même temps. Un développeur a souvent affaire, pendant qu'il développe un module, à des préoccupations telles que la logique métier, la gestion transactionnelle de la persistance, le logging, la sécurité, etc. (Figure). Cela conduit à la présence simultanée d'éléments issus de chaque préoccupation et il en résulte en un enchevêtrement du code.
Un autre exemple pour illustrer l'enchevêtrement du code est la notion d'espace multidimensionnel de préoccupations. Imaginons que les besoins sont projetés sur un espace multidimensionnel où chaque préoccupation représente une dimension. Dans cet espace, chaque préoccupation est indépendante et peut évoluer sans affecter les autres préoccupations. Par exemple, le changement du schéma de sécurité n'affecte pas la logique métier. Cependant, un espace multidimensionnel de préoccupation est réduit dans un espace unidimensionnel d'implémentation (Figure).
Tant que l'espace d'implémentation est unidimensionnel, on se concentre souvent sur l'implémentation de la préoccupation qui a un rôle dominant ; les autres préoccupations sont alors enchevêtrées avec la préoccupation dominante.
I-A-2. Éparpillement du code (code scattering)▲
L'éparpillement du code survient quand une préoccupation est implémentée dans plusieurs modules. Les préoccupations transversales sont, par définition, dispersées à travers plusieurs modules. Par exemple, dans un système utilisant une base de données, le logging est une préoccupation implémentée dans tous les modules qui accèdent à la base (Figure).
I-B. Méthodologie de l'AOP▲
Le développement de logiciels en utilisant l'approche orientée aspect est similaire au développement de logiciels avec d'autres méthodologies (voir figure) : identification des préoccupations, leur implémentation, et leur combinaison pour former le système final. La communauté des chercheurs de l'AOP définit ces trois étapes comme suit :
- Décomposition aspectuelle : les besoins sont ici décomposés pour identifier les préoccupations fonctionnelles et transversales. Par exemple, un développeur pourra identifier les préoccupations suivantes : logique métier, logging, cache, gestion transactionnelle de la persistance, authentification, etc. Ensuite, il pourra décider que seule la logique métier est une préoccupation fonctionnelle. Toutes les autres préoccupations sont des préoccupations transversales au système et qui vont être utilisées par plusieurs modules.
- Implémentation des préoccupations : chaque préoccupation est implémentée indépendamment des autres. Comme pour l'exemple du paragraphe précédent, le développeur aura à implémenter la logique métier, le logging, le cache, la gestion transactionnelle de la persistance, l'authentification, etc.
- Recomposition aspectuelle : des règles de recomposition sont spécifiées en créant des unités appelées aspects. Le processus de recomposition, aussi connu sous le nom de tissage ou d'intégration, utilise ces informations pour composer le système final. Comme pour l'exemple précédent, le développeur peut créer une règle qui s'assure que chaque opération effectuée par la logique métier doit d'abord être mise en journal (loggée).
II. Concepts▲
II-A. Aspect▲
Un aspect est une entité logicielle qui capture une fonctionnalité transversale à une application.
Les trois éléments principaux définis dans un aspect sont les coupes (pointcuts), les codes advice et le mécanisme d'introduction. Les coupes définissent où l'aspect doit être intégré dans une application et les codes advice définissent ce que fait l'aspect (le quoi). Le mécanisme d'introduction permet d'ajouter du contenu structurel dans une application.
Nous allons définir ces concepts avec plus de détails dans les paragraphes suivants.
II-B. Point de jonction (joinpoint)▲
Un point de jonction est un point dans le flot de contrôle d'un programme dans lequel un ou plusieurs aspects peuvent être appliqués.
Bien que la notion de point de jonction soit générale (potentiellement, chaque instruction d'un programme peut être un point de jonction), tous les points dans le flot de contrôle ne sont pas considérés comme utiles pour l'AOP. Les points de jonction sont groupés en fonction de leur type, et seulement un sous-ensemble de tous les types possibles de points de jonction est supporté par les langages orientés aspects.
Bien que la définition d'un point de jonction désigne un moment de l'exécution, cette définition est basée sur la structure d'un programme (ses classes, méthodes, attributs, etc.). Les catégories suivantes décrivent les types de points de jonction communément rencontrés et qui sont indépendants de toute implémentation.
- Les méthodes : dans les langages orientés objet, l'exécution d'un programme peut être considérée comme une séquence d'appels et d'exécution de méthodes. Les différents scénarios d'exécution d'une application peuvent être exprimés en termes de séquences de messages qui déclenchent l'exécution de méthodes. Les appels et les exécutions de méthodes sont donc deux types de points de jonction couramment utilisés.
- Les constructeurs : les constructeurs sont principalement utilisés pour créer les instances des classes d'une application. Comme pour les méthodes, les appels et les exécutions d'un constructeur correspondent à des types de points de jonction.
- Les attributs : les langages orientés aspect considèrent les opérations de lecture et d'écriture sur les attributs comme des types de points de jonction. Un exemple concret d'utilisation de ce type de points de jonction est la gestion transactionnelle de la persistance.
- Les exceptions : les exceptions sont levées (throw) pour signaler une situation d'exécution anormale et elles sont capturées (catch}) pour exécuter un traitement particulier. Ces deux événements (le throw et le catch) sont des points d'exécution très importants dans une application. Ils sont tous les deux considérés par la plupart des langages orientés aspect comme des types de points de jonction.
Les appels et les exécutions de méthodes sont incontestablement les types de points de jonction les plus utilisés dans l'AOP. D'autres éléments de code, comme les boucles FOR et les instructions conditionnelles IF, qui définissent la structure d'un programme sont considérés de très faible granularité pour être utilisés pour définir des points de jonction.
En conclusion, tous les programmes, même les plus simples, contiennent plusieurs points de jonction différents. La tâche du développeur est de sélectionner les points de jonction qui sont utiles pour implémenter un aspect donné. Cette sélection est réalisée grâce à la notion de coupe (pointcut), qui est présentée dans le paragraphe suivant.
II-C. Coupe (pointcut)▲
Dans le paragraphe précédent, nous avons dit que les points de jonction sont des points dans le flot de contrôle d'un programme dans lequel un ou plusieurs aspects peuvent être appliqués. La notion de point de jonction n'est pas suffisante à elle seule pour définir quels points de jonction sont pertinents pour un aspect donné. On a besoin d'une autre notion pour décrire les points de jonction. Cette notion est la coupe.
Une coupe sélectionne un ensemble de points de jonction.
Un langage de programmation orienté aspect se doit de fournir au développeur une structure syntaxique permettant de déclarer une coupe. Cependant chaque langage définit sa propre syntaxe.
Généralement, une coupe est définie avec un langage de patterns qui permet d'indiquer où l'aspect doit être intégré dans l'application en utilisant des quantificateurs, des opérateurs booléens et des caractères joker ou \textit{wildcards} (comme le caractère *).
II-D. Code advice (advice code)▲
Un code advice est un bloc de code définissant le comportement d'un aspect.
Concrètement, un code advice est un bloc d'instructions qui spécifie le comportement de l'aspect. Un code advice est toujours associé à une coupe ou plus exactement aux points de jonction sélectionnés par cette coupe. En effet, un code advice n'est jamais appelé manuellement, mais il est invoqué chaque fois qu'un point de jonction, sélectionné par la coupe à laquelle il est associé, survient.
Un code advice peut être exécuté selon trois modes : avant, après, ou autour d'un point de jonction. Lorsqu'il est exécuté autour du point de jonction, il peut carrément remplacer l'exécution de ce dernier, ou bien lui redonner le contrôle.
II-E. Mécanisme d'introduction▲
Le mécanisme d'introduction est un mécanisme d'extension permettant d'introduire de nouveaux éléments structuraux au code d'une application.
Le mécanisme d'introduction permet d'étendre la structure d'une application et non pas le comportement de cette dernière. En effet, le mécanisme d'introduction ne s'appuie pas sur la notion de coupe, mais va opérer sur des emplacements bien définis dans le programme. On peut dire que le mécanisme d'introduction est pour l'orientée aspect ce que l'héritage est pour l'orientée objet puisque ces deux derniers permettent d'étendre la structure et non pas le comportement de l'application.
II-F. Tissage (weaving)▲
Le tissage (weaving) est le processus qui prend en entrée un ensemble d'aspects et une application de base et fournit en sortie une application dont le comportement et la structure sont étendus par les aspects.
Une application orientée aspect contient des classes et des aspects. L'opération qui prend en entrée les classes et les aspects et produit une application qui intègre les fonctionnalités des classes et des aspects est connue sous le nom de tissage d'aspects (aspect weaving). Le programme qui réalise cette opération est appelé tisseur d'aspects (aspect weaver) ou bien tisseur (weaver) tout court. (Figure)
III. AspectJ▲
III-A. Historique et origine▲
L'histoire d'AspectJ est étroitement liée à celle de la programmation orientée aspect. En effet, ce langage a été développé par la même équipe à l'origine de l'AOP. Un premier prototype d'AspectJ a été réalisé en 1998. Et depuis, plusieurs versions d'AspectJ ont vu le jour, et chacune apportait de nouvelles fonctionnalités et corrigeait les bogues de la précédente. La première version officielle d'AspectJ, désigné AspectJ 1.0, a été réalisée en novembre 2001. Durant cette année, l'AOP a été complètement reconnue par la communauté informatique mondiale, et une édition spéciale du journal Communications of the ACM a été dédiée à l'AOP.
En décembre 2002, le projet AspectJ a quitté XEROX PARC et a rejoint la communauté open source Eclipse. Et depuis, le plugin Eclipse AspectJ Developpement Tools (AJDT) est développé. Ce plugin intègre AspectJ et permet d'écrire, de compiler et d'exécuter des programmes orientés aspects dans l'environnement de développement Eclipse.
III-B. Téléchargement et installation de l'outil▲
AspectJ peut être téléchargé à partir de cette adresse http://www.eclipse.org/aspectj/downloads.php, il se présente sous forme d'un compilateur (ajc) et d'un ensemble de bibliothèques. Pour installer AspectJ il suffit d'exécuter l'installation préalablement téléchargée depuis le lien indiqué précédemment.
À la fin de l'installation, il est recommandé de mettre le répertoire %aspectj_folder%/bin dans la variable d'environnement path afin de pouvoir l'utiliser dans la ligne de commande à partir de n'importe quel chemin.
III-C. Présentation générale▲
AspectJ est aujourd'hui une implémentation orientée aspect phare qui fournit un excellent support pour appréhender les concepts de la programmation orientée aspect. Comme indiqué dans le paragraphe précédent, sa plus grande force réside dans le fait qu'il est issu des travaux de la même équipe à l'origine de l'orientée aspect. AspectJ est donc une extension orientée aspect du langage de programmation Java. Il permet de déclarer des aspects, des coupes, des codes advices et des introductions. Il offre aussi un tisseur d'aspects appelé ajc (pour AspectJCompiler) qui prend en entrée des classes java et des aspects, et produit en sortie des classes dont le comportement est augmenté par les aspects. Aujourd'hui, AspectJ bénéficie d'une multitude d'outils de débogage, d'environnement de développements et de visualisateurs d'aspects. AspectJ permet de définir deux types de transversalités avec les classes de base : transversalité statique (static crosscutting) et transversalité dynamique (dynamic crosscutting).
- Transversalité statique : la transversalité statique consiste à augmenter la structure des classes. Pour cela AspectJ offre la notion de mécanisme d'introduction qui permet entre autres d'ajouter des éléments structuraux comme des attributs ou des méthodes aux classes. Le mécanisme d'introduction d'AspectJ permet aussi d'ajouter des liens entre des classes comme l'héritage et l'implémentation d'interfaces. Concrètement, AspectJ offre un support simple et facile à appréhender pour implémenter ce genre de transversalité.
- Transversalité dynamique : la transversalité dynamique consiste à augmenter le comportement des classes. Pour cela AspectJ offre les notions de coupes et de code advices. Les coupes servent à sélectionner des points précis dans les classes. Et les advices iront se greffer avant, après ou autour de ces points afin d'étendre leur comportement.
Dans ce qui suit, nous allons aborder les principaux concepts de ce langage et leur mise en œuvre pratique. Pour cela, tous les exemples qui seront présentés reposeront sur le diagramme de classes de la figure suivante.
III-D. Point de jonction▲
En réalité, un point de jonction est n'importe quel point d'exécution dans un système. Parmi tous les points de jonction possibles dans un système, on cite de façon non exhaustive : l’appel à une méthode ; l’exécution d'une méthode ; l’affectation de variable ; l’appel au constructeur d'une classe ; une instruction conditionnelle (i.e. IF/THEN/ELSE) ; le traitement d'une exception ; les boucles (i.e. FOR, WHILE, DO/WHILE) ; etc.
Mais en pratique, et par souci de prévenir la dépendance de l'implémentation et la transversalité instable, le modèle de points de jonction adopté par AspectJ n'offre qu'un sous-ensemble de points de jonction possibles. Par exemple, il ne considère pas les boucles comme des points de jonction. L'ensemble des points de jonction offerts par AspectJ est résumé dans le tableau suivant :
Point de jonction |
Description |
---|---|
Method call |
Quand une méthode est appelée |
Method execution |
Quand le corps d'une méthode est exécuté |
Constructor call |
Quand un constructeur est appelé |
Constructor execution |
Quand le corps d'un constructeur est exécuté |
Static initializer execution |
Quand l'initialisation statique d'une classe est exécutée |
Object pre-initialization |
Avant l'initialisation de l'objet |
Object initialization |
Quand l'initialisation d'un objet est exécutée |
Field reference |
Quand un attribut non constant d'une classe est référencé |
Field set |
Quand un attribut d'une classe est modifié |
Handler execution |
Quand un traitement d'une exception est exécuté |
Advice execution |
Quand le code d'un advice est exécuté |
Dans AspectJ, tous les points de jonction ont un contexte associé à eux. Par exemple, le contexte d'un point de jonction correspondant à un appel de méthode contient l'objet appelant, l'objet appelé, et les arguments de la méthode. De la même manière, le contexte d'un point de jonction correspondant au traitement d'une exception contient l'objet courant, et l'exception levée.
III-E. Coupe▲
Dans AspectJ, les coupes correspondent à plusieurs points de jonction dans le flot d'un programme. Par exemple, la coupe :
call
(
void
Point.setX
(
int
))
capture chaque point de jonction correspondant à un appel à la méthode setX() de la classe Point qui ne retourne aucune valeur void et qui a comme paramètre un entier (int).
Une coupe peut être construite à partir d'autres coupes en utilisant les opérateurs and, or et not (respectivement &&, || et !). Par exemple, la coupe :
call
(
void
Point.setX
(
int
)) ||
call
(
void
Point.setY
(
int
))
désigne les points de jonction correspondant à un appel à la méthode Point.setX() ou un appel à la méthode Point.setY().
Les coupes peuvent identifier des points de jonction de différentes classes, en d'autres termes, elles peuvent être transverses aux classes. Par exemple, la coupe :
call
(
void
FigureElement.incrXY
(
int
,int
)) ||
call
(
void
Point.setX
(
int
)) ||
call
(
void
Point.setY
(
int
)) ||
call
(
void
Line.setP1
(
Point)) ||
call
(
void
Line.setP2
(
Point))
capture chaque point de jonction qui est un appel à une des cinq méthodes (la première méthode est une méthode d'interface).
Dans le dernier exemple, la coupe capture tous les points de jonction correspondant au mouvement d'un objet de type FigureElement. AspectJ permet de déclarer des coupes nommées avec le mot-clé pointcut, afin qu'elles puissent être réutilisées sans avoir à les redéfinir. Les instructions suivantes déclarent une coupe nommée :
pointcut move
(
):
call
(
void
FigureElement.incrXY
(
int
,int
)) ||
call
(
void
Point.setX
(
int
)) ||
call
(
void
Point.setY
(
int
)) ||
call
(
void
Line.setP1
(
Point)) ||
call
(
void
Line.setP2
(
Point));
Ainsi, on peut appeler à n'importe quel moment la coupe nommée move().
Jusque là, les coupes que nous avons données étaient basées sur une énumération explicite d'un ensemble de signatures de méthodes. AspectJ offre aussi un mécanisme qui permet de spécifier des coupes en termes de propriétés de méthodes autres que leur nom exact. La façon la plus simple de le faire est d'utiliser les expressions régulières pour exprimer les champs de la signature des méthodes. Par exemple, la coupe :
call
(
void
Point.set*(
..))
capture chaque point de jonction correspondant à un appel d'une méthode ne retournant aucun résultat (void) et appartenant à la classe Point et commençant par la chaine set, quels que soient le type et le nombre de ses paramètres.
La coupe :
call
(
public
*
Line.* (
..))
capture tous les appels aux méthodes publiques (public) de la classe Line.
Les exemples précédents ne font appel qu'à un seul type de coupe qui est l'appel de méthode (i.e. call). Il existe d'autres types de coupes dans AspectJ comme : l'exécution de méthode (ex. : execution(void Point.setX(..))), ou l'accès aux attributs (ex. : get(Point.x)), etc.
III-F. Advice▲
Les coupes capturent les points de jonction, mais elles ne font rien de plus. Pour implémenter un comportement transversal, on utilise les advices. En effet, un advice fait correspondre une coupe (i.e. un ensemble de points de jonction) à un bout de code exécuté à chaque point de jonction de cette coupe.
Un code advice est un bloc d'instructions associé à une coupe. Il est exécuté avant, après ou autour des points de jonction sélectionnés par la coupe qui lui est associée. AspectJ offre trois types de codes advice : before, after et around. Les codes advice de type before (respectivement after) permettent d'introduire un comportement avant (respectivement après) un point de jonction. Cependant les codes advice de type after se déclinent en deux variantes : after returning et after throwing qui signifient respectivement après le retour d'une méthode sans exception et avec exception. Un advice de type around quant à lui définit un bloc d'instructions qui s'exécute autour d'un point de jonction. Il permet éventuellement de remplacer carrément l'exécution d'une méthode. AspectJ fournit la méthode proceed() qui permet de rendre le contrôle de l'exécution au point de jonction dans un code advice de type around. Le code suivant montre un exemple d'advice de type before, qui utilise la coupe move() défini précédemment :
before
(
) : move
(
) {
System.out.println
(
"Figure sur le point d'être déplacée"
);}
Le code suivant montre un exemple d'advice de type around :
around
(
) : call
(
Display.update
(
)) {
if
(!
Display.disabled
(
)) proceed
(
);}
Nous avons dit précédemment qu'à chaque point de jonction est associé un contexte d'exécution (cf. section ) contenant par exemple les arguments de la méthode si le point de jonction en est une. Dans AspectJ, ce contexte est accessible via les trois coupes primitives : this, target, args. Ce contexte s'avère très utile lorsqu'on veut par exemple, accéder aux paramètres d'une méthode dans un code advice. L'advice suivant :
after
(
int
x,int
y) returning : call
(
void
FigureElement.incrXY
(
int
,int
) &&
args
(
x,y) {
System.out.println
(
"la figure a été déplacée de +"
x"+"
,"+y);
}
récupère les arguments de la méthode incrXY(int,int) dans les variables x et y et peut faire n'importe quel traitement avec ces valeurs.
Les coupes primitives this et target permettent respectivement de récupérer l'objet courant et l'objet cible du contexte d'exécution pour le point de jonction. Le tableau suivant résume la signification de l'objet courant, de l'objet cible et des arguments pour chaque type de points de jonction.
Point de jonction |
Objet courant |
Objet cible |
Arguments |
---|---|---|---|
Method Call |
L'objet appelant |
L'objet appelé |
Les arguments de la méthode |
Method Execution |
L'objet appelant |
L'objet appelé |
Les arguments de la méthode |
Constructor Call |
L'objet appelant |
Néant |
Les arguments du constructeur |
Constructor Execution |
L'objet appelant |
L'objet appelant |
Les arguments du constructeur |
Static initializer execution |
Néant |
Néant |
Néant |
Object pre-initialization |
Néant |
Néant |
Les arguments du constructeur |
Object initialization |
L'objet appelant |
L'objet appelant |
Les arguments du constructeur |
Field reference |
L'objet appelant |
L'objet appelé |
Néant |
Field assignment |
L'objet appelant |
L'objet appelé |
La valeur assignée |
Handler execution |
L'objet appelant |
L'objet appelant |
L'exception levée |
Advice execution |
L'aspect appelant |
L'aspect appelant |
Les arguments de l'advice |
III-G. Déclaration intertype▲
Les déclarations intertypes, dans AspectJ, correspondent au mécanisme d'introduction vu précédemment (cf. section II). Elles permettent de déclarer des membres dans des classes, ou de changer la relation d'héritages entre classes. Le code suivant montre quelques exemples de déclaration intertypes. Il s'agit d'ajouter un attribut name de type String et deux méthodes setName() et getName() à la classe Point :
public
String Point.name ;
public
void
Point.setName (
String name ) {
this
.name =
name ; }
public
String Point.getName (
) {
return
name ; }
L'instruction suivante permet de déclarer que les classes Point et Line héritent de la classe GeometricObject :
declare parents : (
Point ||
Line) extends
GeometricObject ;
III-H. Aspect▲
Dans AspectJ. Un aspect contient tous les ingrédients nécessaires pour la définition d'une préoccupation transversale à savoir : les définitions de coupes, les codes advice et les déclarations intertypes. Il peut aussi éventuellement contenir des attributs et des méthodes qui lui sont propres. Le code suivant montre un exemple d'aspect réalisant la préoccupation de mise à jour d'affichage :
aspect UpdateDisplay {
pointcut move
(
FigureElement elem) : target (
elem) &&
(
call (
void
Line.setP1 (
Point) ) ||
call (
void
Line.setP2 (
Point) ) ||
call (
void
Point.setX (
int
) ) ||
call (
void
Point.setY (
int
) ) ||
call (
void
FigureElement.incrXY
(
int
, int
) ) ) ;
after
(
FigureElement elem) returning : move (
elem ) {
Display.update
(
elem);
}
}
IV. Exemple d'application▲
IV-A. Introduction▲
Dans cette section, nous allons nous intéresser à un exemple d'implémentation d'une préoccupation transversale sur un système existant. D'abord, nous allons commencer par décrire le système sur lequel va porter l'exemple. Il s'agit d'un système d'arbre d'expression syntaxique. Nous allons le décrire à l'aide de diagrammes UML accompagnés du code source Java correspondant. Ensuite, nous allons décrire la préoccupation qu'on veut implémenter dans le système. Cette préoccupation consiste en l'implémentation d'un cache qui permettra d'optimiser l'évaluation des arbres syntaxiques en évitant les parcours en profondeur. Enfin, nous allons terminer par comparer l'exécution de l'exemple avec et sans la préoccupation cache.
IV-B. Le système d'arbre d'expression▲
Le système permet de représenter des expressions syntaxiques sous forme d'arbre d'expression syntaxique. Par souci de simplicité, nous avons décidé de ne représenter qu'une seule opération qui est l'opération Plus. Donc, le système est composé de trois classes : Expression, Number et Plus et il est décrit par le diagramme de classe de la figure suivante :
Le code source correspondant aux trois classes du système est très simple et est présenté ci-dessous.
package
org.sdf;
public
abstract
class
Expression {
public
abstract
int
eval
(
);
}
package
org.sdf;
public
class
Plus extends
Expression {
private
Expression leftExpression;
private
Expression rightExpression;
public
Plus
(
Expression leftExpression, Expression rightExpression) {
super
(
);
this
.leftExpression =
leftExpression;
this
.rightExpression =
rightExpression;
}
public
Expression getLeftExpression
(
) {
return
leftExpression;
}
public
void
setLeftExpression
(
Expression leftExpression) {
this
.leftExpression =
leftExpression;
}
public
Expression getRightExpression
(
) {
return
rightExpression;
}
public
void
setRightExpression
(
Expression rightExpression) {
this
.rightExpression =
rightExpression;
}
@Override
public
int
eval
(
) {
System.out.println
(
"Evaluation d'un noeud"
);
return
leftExpression.eval
(
) +
rightExpression.eval
(
);
}
}
package
org.sdf;
public
class
Number extends
Expression {
private
int
value;
public
Number
(
int
value) {
this
.value =
value;
}
public
void
setValue
(
int
value) {
this
.value =
value;
}
@Override
public
int
eval
(
) {
System.out.println
(
"Evaluation d'un noeud"
);
return
value;
}
}
Si on exécute le programme suivant
package
org.sdf;
public
class
Main {
/**
*
@param
args
*/
public
static
void
main
(
String[] args) {
Number dix =
new
Number
(
10
);
Number neuf =
new
Number
(
9
);
Number cinq =
new
Number
(
5
);
Plus plus1 =
new
Plus
(
dix,neuf);
Plus plus2 =
new
Plus
(
plus1,cinq);
// au début, defaut de cache obligatoire
System.out.println
(
"premier appel"
);
System.out.println
(
plus2.eval
(
));
// on a un cache valide, donc on le consulte au lieu de parcourir l'arbre
System.out.println
(
"deuxième appel"
);
System.out.println
(
plus2.eval
(
));
// on va altérer la valeur du nœud 5
// qui va conduire à invalider son cache ainsi que celui de operation2
cinq.setValue
(
4
);
System.out.println
(
"troisième appel"
);
System.out.println
(
plus2.eval
(
));
}
}
on aura comme résultat l'arbre de la figure suivante :
IV-C. La préoccupation « cache »▲
Supposons que nous voulons étendre le système pour qu'il supporte un mécanisme de cache. Le principe du cache dans l'arbre d'expression est simple : on ajoute une variable cache au niveau de chaque nœud, et à chaque appel de eval(), on vérifie si la valeur du cache est valide. Si c'est le cas, on n'aura pas besoin de parcourir les sous-arbres gauches et droits.
Apporter un mécanisme de cache au système est certes très bénéfique côté performance. Mais malheureusement, le code source sera moins lisible. Car, d'une part, il y aura un enchevêtrement du code source métier (i.e. code source original du système d'arbre d'expression) et du code source de la préoccupation cache et d'autre part, la préoccupation cache sera dispersée à travers les classes du système. En effet, pour implémenter une telle préoccupation, on aura à déclarer une variable cache dans la classe Expression qui détiendra la valeur courante du cache, et des méthodes pour accéder a cette variable. De plus, on devra implémenter un mécanisme d'invalidation du cache dans toutes les sous-classes de la classe Expression au cas où la valeur du cache serait invalide. Et puis, on devra changer l'implémentation des méthodes eval() dans toutes les classes de façon à ce qu'elles renvoient la valeur du cache s'il est valide.
C'est là où l'AOP intervient, en apportant une solution efficace et élégante aux problèmes cités ci-dessus. La préoccupation cache sera implémentée dans un seul module et non éparpillée à travers plusieurs classes.
Comme précisé ci-dessus, pour gérer le cache, on devra déclarer une variable cache qui détient la valeur courante du cache, ainsi qu'une variable booléenne qui indique si le cache est valide ou pas. Ceci est réalisé grâce au mécanisme de déclaration intertypes (cf. section ) comme suit :
private
int
Expression.cache;
private
boolean
Expression.cacheValid =
false
;
public
int
Expression.getCache
(
) {
return
cache;
}
public
void
Expression.setCache
(
int
cache) {
this
.cache =
cache;
}
public
boolean
Expression.isCacheValid
(
) {
return
cacheValid;
}
public
void
Expression.validateCache
(
) {
this
.cacheValid =
true
;
}
En plus de ces deux variables, chaque nœud devra connaitre l'identité de son père afin qu'il puisse lui signaliser que son cache n'est plus valide, ce qui va impliquer aussi l'invalidation du cache du père et vice-versa jusqu'à la racine. Ceci est fait en déclarant une variable appelée ancestor qui pointera vers le père du nœud, et une méthode qui permet d'invalider le cache des ascendants (s'ils existent) en cascade, comme suit :
private
Expression Expression.ancestor =
null
;
public
void
Expression.invalidateCache
(
) {
cacheValid =
false
;
if
(
this
.getAncestor
(
)!=
null
) this
.getAncestor
(
).invalidateCache
(
);
}
public
Expression Expression.getAncestor
(
) {
return
ancestor;
}
public
void
Expression.setAncestor
(
Expression ancestor) {
this
.ancestor =
ancestor;
}
L'invalidation du cache devra survenir après chaque changement de valeur d'un nœud. Le changement de valeur d'un nœud consiste en l'appel d'une des méthodes :
- Number.setValue();
- Plus.setLeftExpression();
- Plus.setRightExpression();
En AspectJ, cette coupe s'exprime comme suit :
pointcut changeValue
(
Expression exp):
target
(
exp) &&
(
call
(
public
void
Number.setValue
(
int
)) ||
call
(
public
void
Plus.setLeftExpression
(
Expression)) ||
call
(
public
void
Plus.setRightExpression
(
Expression))
);
Et l'advice correspondant à cette coupe devra appeler la méthode invalidateCache() sur l'objet exp (objet sur lequel la méthode a été appelée et récupérée avec la coupe primitive target) après chaque occurrence de l'un des points de jonction de la coupe. Il est déclaré comme suit :
after
(
Expression exp):changeValue
(
exp) {
exp.invalidateCache
(
);
}
Afin de pouvoir consulter le cache avant d'évaluer les fils gauches et droits, on devra capturer chaque point de jonction correspondant à un appel à la méthode eval(). Ceci est effectué grâce à la coupe suivante :
pointcut evaluation
(
Expression exp):
target
(
exp) &&
call
(
public
int
Expression.eval
(
));
Et l'advice correspondant à cette coupe doit d'abord voir si le cache est valide. Si ce n'est pas le cas, il appelle la méthode originale avec proceed() pour évaluer la valeur des sous-arbres, puis il affecte cette valeur au cache, le valide et retourne sa valeur. Il est déclaré comme suit :
int
around
(
Expression exp):evaluation
(
exp) {
if
(!
exp.isCacheValid
(
)) {
int
result =
proceed
(
exp);
exp.setCache
(
result);
exp.validateCache
(
);
}
return
exp.getCache
(
);
}
La dernière chose à faire consiste à créer un lien entre un nœud et son père. Pour cela, on devra capturer tous les points de jonction correspondant au constructeur de la classe Plus. Ceci est fait avec la coupe suivante :
pointcut PlusCreation
(
Plus exp):
this
(
exp) &&
execution
(
Plus.new
(
Expression,Expression));
Et l'advice correspondant devra lier le père aux fils, en utilisant la méthode setAncestor() déclarée précédemment, comme suit :
after
(
Plus exp) : PlusCreation
(
exp) {
exp.getLeftExpression
(
).setAncestor
(
exp);
exp.getRightExpression
(
).setAncestor
(
exp);
}
Voilà, il ne reste plus qu'à mettre tous ces petits bouts de code dans un même module (ou aspect). Ce qui va résulter en ceci :
package
org.sdf;
public
aspect Caching {
private
int
Expression.cache;
private
boolean
Expression.cacheValid =
false
;
private
Expression Expression.ancestor =
null
;
public
int
Expression.getCache
(
) {
return
cache;
}
public
void
Expression.setCache
(
int
cache) {
this
.cache =
cache;
}
public
boolean
Expression.isCacheValid
(
) {
return
cacheValid;
}
public
void
Expression.validateCache
(
) {
this
.cacheValid =
true
;
}
public
void
Expression.invalidateCache
(
) {
cacheValid =
false
;
if
(
this
.getAncestor
(
)!=
null
) this
.getAncestor
(
).invalidateCache
(
);
}
public
Expression Expression.getAncestor
(
) {
return
ancestor;
}
public
void
Expression.setAncestor
(
Expression ancestor) {
this
.ancestor =
ancestor;
}
pointcut changeValue
(
Expression exp):
target
(
exp) &&
(
call
(
public
void
Number.setValue
(
int
)) ||
call
(
public
void
Plus.setLeftExpression
(
Expression)) ||
call
(
public
void
Plus.setRightExpression
(
Expression))
);
after
(
Expression exp):changeValue
(
exp) {
exp.invalidateCache
(
);
}
pointcut evaluation
(
Expression exp):
target
(
exp) &&
call
(
public
int
Expression.eval
(
));
int
around
(
Expression exp):evaluation
(
exp) {
if
(!
exp.isCacheValid
(
)) {
int
result =
proceed
(
exp);
exp.setCache
(
result);
exp.validateCache
(
);
}
return
exp.getCache
(
);
}
pointcut PlusCreation
(
Plus exp):
this
(
exp) &&
execution
(
Plus.new
(
Expression,Expression));
after
(
Plus exp) : PlusCreation
(
exp) {
exp.getLeftExpression
(
).setAncestor
(
exp);
exp.getRightExpression
(
).setAncestor
(
exp);
}
}
IV-D. Exécution sans cache▲
Pour ne pas prendre en compte l'aspect Caching, le fichier Caching.aj ne doit pas être indiqué au compilateur ajc lors de la compilation. Les commandes suivantes servent à compiler les classes sans la prise en compte de l'aspect Caching et à exécuter l'application :
ajc -cp c:\aspectj1.6\lib\aspectjrt.jar -source 1.6 org/sdf/*.java
java org.sdf.Main
Ce qui donne le résultat suivant après l'exécution :
premier appel
Evaluation d'un noeud
Evaluation d'un noeud
Evaluation d'un noeud
Evaluation d'un noeud
Evaluation d'un noeud
24
deuxième appel
Evaluation d'un noeud
Evaluation d'un noeud
Evaluation d'un noeud
Evaluation d'un noeud
Evaluation d'un noeud
24
troisième appel
Evaluation d'un noeud
Evaluation d'un noeud
Evaluation d'un noeud
Evaluation d'un noeud
Evaluation d'un noeud
23
On remarque qu'à chaque appel, on devra visiter les cinq nœuds de l'arbre.
IV-E. Exécution avec cache▲
Pour prendre en compte l'aspect Caching, le fichier Caching.aj doit être indiqué au compilateur ajc lors de la compilation. Les commandes suivantes servent à compiler les classes avec la prise en compte du tissage de l'aspect Caching et à exécuter l'application :
ajc -cp c:\aspectj1.6\lib\aspectjrt.jar -source 1.6 org/sdf/*.java org/sdf/*.aj
java -cp c:\aspectj1.6\lib\aspectjrt.jar;. org.sdf.Main
Ce qui donne le résultat suivant après l'exécution :
premier appel
Evaluation d'un noeud
Evaluation d'un noeud
Evaluation d'un noeud
Evaluation d'un noeud
Evaluation d'un noeud
24
deuxième appel
24
troisième appel
Evaluation d'un noeud
Evaluation d'un noeud
23
On remarque qu'au premier appel de eval(), on devra créer 5 défauts de cache obligatoires. Mais au deuxième appel aucun nœud n'est évalué, la valeur 24 est extraite du cache et aucun parcours de l'arbre n'est effectué. Au troisième appel la valeur du nœud 5 est altérée donc la valeur de son cache et aussi celle du cache de son père (le nœud plus 2) sont invalidées, ce qui résulte en la réévaluation de seulement deux nœuds dans l'arbre.
V. Conclusion▲
Dans cet article, nous avons fait un survol sur la programmation orientée aspect. Après avoir expliqué les problèmes auxquels l'AOP apporte des solutions efficaces dans la section, nous avons abordé les principaux concepts de ce paradigme dans la section en nous basant sur des références plus ou moins récentes. Ensuite, nous avons présenté dans la section le langage AspectJ, ses origines, les notions qu'il apporte au langage de programmation Java. Nous avons vu la syntaxe de ses principales composantes accompagnée de plusieurs exemples illustratifs.
La section présente un exemple concret d'implémentation d'une préoccupation transversale dans un système existant. Il s'agit d'implémenter un schéma de cache dans un système d'arbre d'expression, afin d'éviter des parcours en profondeur. Pour cela, le système a subi une évolution sans perdre le moindre degré de maintenabilité ou de compréhensibilité. La préoccupation transversale Cache a été exprimée dans le langage AspectJ. Elle a été encapsulée dans un seul module et le programme principal n'a subi aucune modification. Les résultats empiriques ont montré la faisabilité de cette approche.
VI. Téléchargement des sources▲
VII. Liens connexes▲
VIII. Remerciements▲
Je remercie La Zélie et jacques_jean pour l'excellent travail qu'ils ont effectué pour la relecture orthographique ainsi que Ricky81 et Baptiste Wicht pour leurs conseils et encouragements.
IX. Commentaires des lecteurs▲
Vos commentaires sont les bienvenus dans cette discussion 10 commentaires