AGYiNwxLxGUClQhDKdus 23 acb267461af2b857d1cc1067791faf86 file


966KB taille 3 téléchargements 799 vues
INGENIERIE DES LOGICIELS : LES NOUVEAUX PARADIGMES

Dr Djamel Meslati Université Badji Mokhtar Annaba

Février 2007 Version 0 Non corrigée

PREFACE Depuis près d’une décennie le génie logiciel a amorcé son retour au devant de la scène grâce à des avancées significatives au niveau des paradigmes et aux travaux d’unification et de d’uniformisation entrepris par le consortium OMG (Object Management Group) sur le processus de développement des logiciels (dont, principalement l’ingénierie des modèles). Les spécialistes s’accordent à dire que ce retour, qui s’est basé initialement sur l’approche orientée objet, continue d’avoir un impact majeur sur les différentes disciplines de l’informatique grâce à trois nouveaux paradigmes : les modèles de conception (Patterns), les composants logiciels (Software components) et la séparation avancée des préoccupations (Advanced separation of concerns). L’objectif de ce cours est de présenter aux candidats un état de l’art sur ces trois nouveaux paradigmes et d’amorcer des discussions et des réflexions sur les divers liens et rapports entre ces paradigmes au sein du génie logiciel. Il est question aussi d’une maîtrise de ces paradigmes au niveau pratique avec un approfondissement de certains aspects, que le candidat doit entreprendre, pouvant déboucher sur des axes de recherche prometteurs. Les motivations de ce cours sont nombreuses, en effet, on assiste à une prolifération des systèmes à forte dominance logicielle tels que les téléphones portables, les systèmes de navigation, les systèmes multimédias, les systèmes d’assistance médicale, etc. Notre dépendance vis-à-vis d’eux ne cesse de croître et la complexité des logiciels qui les pilotent a atteint des proportions inimaginables, il y a encore deux décennies. Face à cette complexité la communauté du génie logiciel propose diverses approches et des paradigmes qu’il est primordial de maîtriser. Le lecteur, qu’il soit un futur producteur de logiciels ou un futur enseignant des diverses disciplines de l’informatique, se doit de connaître et maîtriser les paradigmes clés des logiciels d’aujourd’hui et de demain. Il doit aussi, avec une certaine mesure, appréhender, à travers les nouveaux paradigmes, les tendances pour lesquelles on prédit, déjà, un impact majeur sur l’informatique de demain.

CONTENU PREFACE

3

CONTENU

4

1 INTRODUCTION A LA COMPLEXITE

6

1.1 SYSTEME, MODELE ET MODELISATION 1.2 QU’EST CE QUE LA COMPLEXITE? 1.3 COMMENT RESOUDRE LES PROBLEMES DE LA COMPLEXITE ? 1.3.1 LES APPROCHES BASEES PROCESSUS 1.3.2 LES APPROCHES BASEES PRODUIT

6 8 14 16 24

2 LA SEPARATION AVANCEE DES PREOCCUPATIONS

25

2.1 LA PROGRAMMATION ORIENTEE ASPECTS 2.2 LA COMPOSITION DE FILTRES 2.3 LA SEPARATION MULTIDIMENSIONNELLE

25 30 37

3 LES COMPOSANTS LOGICIELS

46

3.1 DEFINITION ET OBJECTIFS 3.2 CARACTERISTIQUES 3.3 COMPOSANTS ET CYCLE DE VIE D’UN LOGICIEL 3.4 EXEMPLES JAVABEANS ET EJB CORBA COM ET DCOM FRACTAL

46 47 48 50 50 53 55 55

4 LES MODELES DE CONCEPTION

58

4.1 DEFINITION ET OBJECTIFS 4.2 CARACTERISTIQUES ET QUALITES 4.3 CLASSIFICATION 4.4 EXEMPLES DE MODELES DE CONCEPTION

58 59 61 62

5 APPORT ET RAPPORT ENTRE PARADIGMES

66

5.1 IMPACT DES PARADIGMES

67

5.1.1 IMPACT DE LA STABILITE/VARIABILITE 5.1.2 IMPACT DE LA REUTILISATION 5.1.3 IMPACT DE LA MDA 5.1.4 IMPACT DE LA SEPARATION AVANCEE DES PREOCCUPATIONS 5.1.5 IMPACT DES COMPOSANTS LOGICIELS 5.1.6 IMPACT DES MODELES DE CONCEPTION 5.2 RAPPORT ENTRE PARADIGMES 5.3 VERS UNE HYBRIDATION DES PARADIGMES

67 68 68 70 71 72 73 73

6 TENDANCES FUTURES

75

REFERENCES

76

1 INTRODUCTION A LA COMPLEXITE De nos jours, on assiste à une prolifération des systèmes à forte dominance logicielle tels que les téléphones portables, les systèmes de navigation, les systèmes multimédias, les systèmes d’assistance médicale, etc. Notre dépendance vis-à-vis d’eux ne cesse de croître et la complexité des logiciels qui les pilotent a atteint des proportions inimaginables, il y a encore deux décennies. De nombreux auteurs identifient la complexité comme un problème crucial pour l’ingénierie des logiciels et prônent une approche, désormais, pluridisciplinaire pour faire face aux difficultés inhérentes au développement et à l’évolution des logiciels. Dans ce chapitre, nous présentons un aperçu sur la complexité et sur les approches qui permettent de la gérer. Dans un premier temps, nous décrirons brièvement la notion de modèles et de modélisation, puis nous introduisons la notion de complexité, les problèmes qu’elle engendre et enfin nous situons les approches actuelles qui permettent de réduire la complexité ou du moins la gérer.

1.1 Système, modèle et modélisation Un système est un ensemble pouvant inclure des programmes, des ordinateurs, des combinaisons de parties de systèmes, des personnes, des entreprises, etc. [Omg 03]. Un système dans un environnement donné peut être perçu comme une collection d’entités simples ou complexes, figées ou ayant des comportements, stables ou évolutives, entre lesquelles existent des liens et règnent des interactions de natures diverses. Le système interagit aussi avec son environnement et ce dernier peut être vu, à son tour, comme un système. Les entités peuvent être vivantes, abstraites ou physiques, des objets de la nature, des objets conçus par l’homme, etc. Souvent, on s’intéresse à une partie de la réalité (i.e. un système et son environnement) et on la désigne par domaine d’intérêt. Un modèle est une description conceptuelle d’un système et de son environnement. Par conceptuel, on sous entend l’utilisation de concepts pour décrire les entités de la réalité, leurs structures, leurs comportements, leurs liens et leurs interactions mutuelles ou avec l’environnement. Les modèles sont concrétisés par des diagrammes, des formules, des descriptions textuelles ou une combinaison des trois. Par exemple, un modèle orienté objets d’une réalité, représente les entités par des objets, leurs ressemblances par les classes, leurs comportements par les méthodes, leurs liens par des liens de référence, de composition ou

d’héritage. Quant aux interactions, elles sont matérialisées par l’envoi et la réception de messages, la capture d’événements, etc. [Boo 93]. L’ensemble des concepts disponibles dans une approche donnée est désigné par modèle, ou modèle de données. Pour éviter toute confusion, nous l’appellerons métamodèle (conformément à l’approche ingénierie des modèles [Omg 03]) ou paradigme. Un système et son environnement peuvent être représentés par plusieurs modèles à la fois, dont certains peuvent être physiques et d’autres virtuels. Un système logiciel n’est autre qu’un ensemble de modèles qui décrivent, chacun, une facette de ce dernier : sa conception, son utilisation, son interface, sa documentation mais, bien sûr, aussi son programme [Kle 03]. Un ordinateur, un système électronique d’asservissement ou un robot sont aussi des modèles qui représentent des entités réelles ou imaginées par l’homme. Les modèles conçus par l’homme sont destinés à être incorporés dans la réalité elle-même et à interagir avec celle-ci dans le but d’assurer une fonctionnalité ou un objectif donnés. La modélisation désigne le processus par lequel l’homme utilise les différents concepts d’un paradigme pour produire un modèle d’un système donné. Le schéma de la figure 1.1 illustre nos propos avec un exemple. Dans ce dernier, il est question de modéliser les entités et les activités impliquées dans la gestion du trafic d’un aéroport. La figure montre des interactions diverses ainsi que plusieurs modèles destinés à capturer différentes facettes de la réalité.

Modélisation

Réalité

Piste, couloir aérien, technicien ?

Modèles Avion Radar Diagramme d’activité

Développeur Interface de contrôle

Description de la plateforme

Qualité des services

Utilisateur Signifie interactions

Figure 1.1. Modélisation du trafic aérien De par sa nature même, le processus de modélisation est un processus biaisé. En effet, l’homme représente la réalité en fonction :

›

De sa perception et sa compréhension de celle-ci

›

Du paradigme utilisé et des concepts qu’il renferme

›

Des objectifs qu’il souhaite atteindre (i.e. les besoins)

De ce fait, le modèle n’est pas une représentation totalement conforme à la réalité mais une représentation plus ou moins proche de celle-ci pouvant être : ›

Inconsistante si la compréhension que l’homme a de la réalité est fausse

›

Abstraite ou limitée selon la perception. Abstraite veut dire d’un niveau de détail donné, qui ignore les détails du niveau inférieur. Limitée signifie que la représentation ne couvre que le domaine d’intérêt. La frontière entre ce dernier et le reste de la réalité peut varier dans le temps et sa détermination n’est pas une tâche triviale.

›

D’un certain écart sémantique si le paradigme utilisé n’offre pas des concepts adéquats qui capturent toute la sémantique de leurs correspondants réels ou l’utilisation d’un concept au lieu d’un autre plus adéquat.

›

Ne couvre pas totalement les besoins

Atteindre un modèle totalement conforme à la réalité demeure un idéal même lorsqu’on se limite à quelques besoins spécifiques. Depuis quelques années, les systèmes conçus par l’homme apparaissent comme des systèmes à forte dominance logicielle. Autrement dit, la partie physique est devenue proportionnellement très faible par rapport à la partie logicielle. Cela est du principalement à la facilité relative, comparé à la partie physique, avec laquelle un logiciel peut être adapté ou modifié. Ce décalage des fonctionnalités du physique au logiciel est une conséquence : ›

du besoin grandissant d’adaptabilité et de variabilité des systèmes.

›

des contraintes sur les délais de mise sur le marché.

›

des contraintes sur le coût.

›

etc.

Ce décalage a des avantages indéniables, cependant, vu du côté ingénierie du logiciel, il constitue un facteur de complexité supplémentaire qui vient s’ajouter à une complexité déjà croissante qui est due à la prolifération des systèmes logiciels dans notre vie quotidienne et, par conséquent, la diversité des fonctionnalités qu’ils doivent garantir. Dans ce qui suit nous présentons un aperçu sur la complexité.

1.2 Qu’est ce que la complexité? La complexité est un problème principal que ingénierie des logiciels tente de résoudre ou du moins gérer par les diverses outils et les méthodes de développement et de maintenance. On considère qu’un système logiciel est complexe s’il requière une longue description. Celleci peut être textuelle comme elle peut utiliser des concepts plus abstraits qui permettent de décrire les dépendances et les différentes relations structurelles et comportementales entre

les entités qui constituent un système donné. Plus le nombre de dépendances entre les entités est grand plus la description est longue et de même pour la complexité. Souvent, lors du développement d’un système, on ajoute des entités d’un niveau supplémentaire qui peuvent ne pas persister dans la version finale du système. Ces niveaux permettent d’introduire certaines qualités dans le système. Par exemple, l’utilisation de modules (partie d’un système) permet de facilité l’évolution d’un système. Ainsi, lors de la phase de maintenance, la réalisation d’un changement donné, commence par isoler un nombre de modules aussi petits que possible de tel sorte à limiter le nombre d’altérations à effectuer. Idéalement, la répartition du système en modules devrait se faire de telle sorte que tout changement futur n’affectera qu’un seul module. Malheureusement, aussi bien la nature des changements que les liens de dépendances entre les modules font que la réalisation d’un changement altère souvent une multitude de modules. Dans certains cas le changement se limite à remplacer un module par un autre. L’introduction de nouvelles entités dans la description d’un système peut augmenter le nombre de dépendances entre les différentes entités. Cependant avec une approche méthodique bien étudiée, il est possible de réduire le taux de dépendances. Tout système logiciel est dote d’une architecture qui présente une vue du logiciel sous forme de modules composants et de connecteurs. Les composants encapsulent des ensembles de fonctionnalités et les connecteurs permettent d’effectuer des interactions entre les composants. Le système est à même d’éxiber certaines qualités selon la manière avec laquelle il est construit à partir des composants et des connecteurs. Nombreux auteurs relient la notion d’architecture avec les attributs qualités d’un système. En effet, la fiabilité ou la modifiabilité dépendent de la décomposition du système logiciel en composants et de leur connections. Un des points importants à signaler est que différentes structurations des composants peut affecter les attributs qualités sans pour autant affecter les fonctionnalités qu’un système donné réalise. Il est donc possible de remanier une architecture, dans certaines limites, afin de réduire la complexité ou la décaler d’un niveau du système à un autre. Ainsi, on peut augmenter la complexité d’une architecture tout en réduisant celle des modules composants pris séparément. Par exemple, une décomposition hiérarchique d’un système nous permet de distribuer la complexité sur une multitude de composants ce qui permet : -

La division du travail de développement sur plusieurs intervenants

-

L’augmentation de l’autonomie des intervenants dans le développement d’un système. En effet, la diminution des dépendances entre les composants donne plus de liberté aux intervenants dans la prise de décisions concernant les composants dont ils sont responsables (i.e. il y a moins d’interactions lors de la prise des décisions).

Tout système a une complexité réelle et une complexité apparente. La complexité réelle concerne le système sous forme exécutable ainsi que sa plateforme support. Elle se mesure en terme d’instructions exécutées, d’accès mémoire, d’appels de routines, …, elle dépend des objectifs du système à concevoir et augmente avec le nombre de fonctionnalités que le

système assure et la nature des données présentes au moment de l’exécution ainsi que la plateforme support. La complexité apparente, quant à elle, concerne la description non exécutable d’un système, elle dépend de notre compréhension, notre expérience et les paradigmes qu’on utilise. Il est possible de réduire la complexité apparente d’un système logiciel par le choix des langages de description qui peuvent être des mots, des graphiques ou une combinaison des deux. Ainsi, si on peut réduire la description d’un système sans altérer le sens de ce qui est décrit, on diminue de fait sa complexité apparente. Lorsqu’on utilise le langage naturel, on peut réduire la complexité apparente par l’introduction d’une terminologie standard ou un seul mot peut véhiculer plusieurs informations. Par exemple dans ingénierie de logiciels les patrons de conception (design patterns) réduisent grandement la complexité apparente grâce à l’hypothèse que les intervenants connaissent ces patrons. En réduisant la complexité apparente d’un système, en augmente sa compréhensibilité par les intervenants même si sa structure interne demeure complexe. La complexité est toujours reliée à la granularité et au contexte. Selon Gell-Mann, lors de la définition de la complexité, il est toujours nécessaire de spécifier un niveau de détail qui sera une limite inférieure considérée durant la description du système. Gell-Mann utilise l’analogie avec le zoom effectué sur une photographie. Pour voir plus de détails, il faut effectuer un agrandissement, mais cet agrandissement a une limite supérieure qui correspond aux pixels de la photographie. Lorsque on se focalise sur une partie de la photographie avec un agrandissement, il est possible de tirer une information de cette partie de la photographie. Cependant à ce niveau de détail, il n’est pas possible de cerner le message que la photographie, dans sa totalité, transmet, il faut alors avoir la possibilité de voir la photographie dans sa totalité. Le même principe est valable pour les diagrammes d’UML vu que chaque modèle écarte nécessairement certains détails du système en cours de description. Par exemple le diagramme des composants écarte les détails concernant les objets, leurs attributs, leurs méthodes et leurs interactions avec les autres. Un diagramme de séquence représente l’interaction des objets mais écartes les attributs des objets et certaines de leurs méthodes. Ainsi, seules les méthodes qui concernent l’interaction son montrées et de même pour la communication avec d’autres objets non impliqués dans le scénario en cours de modélisation. Notons que chercher à comprendre un système en utilisant des modèles de bas niveau ou le code source, est comparable à chercher à saisir le message d’une photographie en se focalisant sur les grains. Comme signalé précédemment, la complexité dépend de la granularité et du contexte. En effet, le contexte peut changer la complexité apparente d’un système. Pour comprendre cela, considérons l’exemple de la figure 1.2 où il est question d’ordonner, de la moins complexe à la plus complexe, les matrices de spécification de dépendances entre cinq entités (ou module composant).

A B C D E

A { x x x x

B x { x x x

C x x { x x

D x x x { x

E x x x x {

A B C D E

A { x x x x

B { x x x

a

A B C D E

A { x x x

B

D

E

x { { x

x

d

{ x x

D

{ x

E

{

A B C D E

A { x

B x { x

b C

{ x

C

{

A B C D E

A {

B

x { x

D

E

x { x

x {

c C

D

E

{ { { {

e

C

Description de b A dépend de B, A dépend de C, A dépend de D, A dépend de E, B dépend de C, ... D dépend de E

B ne dépend pas de C ne dépend pas de C ne dépend pas de ... E ne dépend pas de

Figure 1.2. Variation du degré de la complexité La réponse à la question précédente dépend du contexte de celle-ci, autrement dit, que sommes nous entrain de décrire. Si on s’intéresse aux matrices elles même sans aucune interprétation des éléments, alors les matrices les moins complexes sont a et e car elles sont les plus simples à décrire. La matrice a est totalement connectée et e est totalement déconnectée. Notons qu’il n’est pas nécessaire de donner plus de description. Même lorsqu’on réarrange les lignes est les colonnes l’apparence des matrices ne change pas et donc la description des systèmes sous-jacents ne change pas. La matrice b nécessite plus de description. Dans sa forme actuelle, on peut la décrire comme totalement connectée sous la diagonale. Malheureusement, cette description dépend de l’ordre des lignes et des colonnes. Si on permute A et B (en ligne et en colonne). Le x apparaîtra sur la diagonale, est donc la description générique donnée précédemment est incorrecte. Cette constatation nous oblige à décrire la matrice b élément par élément ce qui produit une description plus longue (Une vingtaine de relations) que celle des matrices a et e. La description de b peut être simplifiée si on change de contexte, en considérant que b décrit un système ayant une architecture en couches (les couches étant A, B, C, D et E). On assigne alors à chaque couche un ordre hiérarchique et on spécifie que chaque couche d’un niveau hiérarchique dépend de toutes les couches de niveau inférieur et que les couches inférieures ne dépendent pas des couches supérieures. La description est plus complexe que celle de la matrice a et e mais moins complexe que la description initiale qui explicite les vingt relations. La matrice c contient uniquement huit relations, donc sa description est moins importante que celle de b. Cependant, partant du même principe, on peut chercher une description en termes de patrons de telle sorte à réduire la complexité tout en augmentant la compréhensibilité. En effet, dans les architecture en couches classiques, seules les couches adjacentes sont dépendantes l’une de l’autre. Il suffit alors de considérer que la couche adjacente de A est B, celle de B est C, ... et cela est suffisant pour décrire leurs interactions.

A, A, B, D,

Cette nouvelle description du système représenté par la matrice c a la même complexité que la description de celui représenté par b. La matrice d contient sept relations ce qui devrait nécessité moins de description que le cas b et c lorsqu’on n’utilise aucun patrons. Cependant, vu que les relations sont quelques peu aléatoires, il n’est pas possible de trouver un patron à substituer à la description du système représenté par d qui demeure la plus longue. Sans les patrons, nous sommes obligé de représenter un système en termes de chaque relation. Par conséquent, la description d’un système est d’autant plus longue que le nombre de relations entre ces parties composantes est important. Lorsqu’on change de contexte la complexité de chaque système change. Si on perçoit un système quelconque en termes d’effort de conception, on constate que plus il y a de relations entre les éléments qui le constituent, plus il y a des dépendances et, par conséquent, plus le processus de conception est complexe. La complexité de l’effort de conception est ainsi liée à l’interdépendance des éléments qui constituent un système. Dans le cas de la matrice a de la figure 1.2, chaque élément a une interdépendance avec les autres. Par conséquent, opérer un seul changement de conception engendre une vague de répercutions sur chacun des autres éléments et, par la suite, crée potentiellement des boucles de répercutions vu que le changement des autres éléments déclenchera d’autres changements. On qualifie parfois les changements qui ont des répercutions croissantes par changements envahissants (invasive changes en anglais). Le système décrit par la matrice e de la figure 1.2 est le plus simple à concevoir car il n’y a aucune dépendance entre les éléments. Jusqu’à présent nous n’avons pas tenu compte de la nature (le type) des relations entre les éléments d’un système. Les discussions précédentes supposaient que les relations étaient homogènes. De ce fait, on s’est focalisé sur l’interconnectivité des éléments sans tenir compte de la nature de cette interconnectivité. Si chaque relation représente une interface différente entre les éléments alors on peut conclure que plus il y a de relations plus le système est complexe et de même pour sa description. La recherche de patrons est, encore une fois, de mise. Il est possible de changer de contexte en se basant sur la granularité de l’information. Supposons que la matrice e de la figure 1.3, représente le développement de cinq systèmes d’exploitation totalement indépendants proposé par des firmes entre lesquelles il n’existe aucune coopération. Ainsi, les décisions de conception d’une firme n’ont aucune influence sur celles des autres. Malheureusement, une information supplémentaire indique que ces systèmes sont dédiés à une plateforme physique particulière produite par une firme donnée que l’on nommera FP. Les firmes qui produisent les systèmes d’exploitations dépendent des décisions de conception faite par les concepteurs de la firme FP. Cette relation est exprimée dans le tableau suivant:

Plateforme FP SE A SE B SE C SE D SE E

Plateforme FP O

SE A x O

SE B x

SE C x

SE D x

SE E x

O O O O

Figure 1.3. Dépendance de quelques systèmes d’exploitation Qu’arrivera-t-il si les concepteurs d’une firme coopèrent étroitement avec ceux de la firme FP? Supposons que la firme qui produit le système d’exploitation A est celle qui coopère avec FP. Dans ce cas les décisions de conception de A peuvent influencer celles de la plateforme FP qui influenceront, à leur tour, les décisions des firmes qui produisent les autres systèmes d’exploitation. Ainsi si on se place dans un contexte où on ignore la relation entre les deux firmes on obtient une description très simple, à l’opposé, la description sera plus longue lorsqu’on en tient compte. Par ailleurs, il est possible de changer de contexte en changeant de granularité. Par exemple la matrice a de la figure 1.4 montre une dépendance de tous les éléments de A. En développant A nous constatons une organisation interne composée de 4 éléments ayant une relation d’une complexité importante (voir matrice b de la figure 1.4). On obtient aussi plus d’information sur quels éléments de A dépendent B, C, D et E. Ainsi, B et C dépendent du même élément tandis que D et E dépendent, chacun, d’un élément de A. En conclusion, on peut dire sur quel élément de A les décisions de conception de B, C, D et E auront des influences. Notons que notre description donnée dans la matrice b est plus complexe que celle de a.

A O x x x x

A B C D E

B

C

D

E

O O O O

a A1 A2 A3 A4 B C D E

A1 O x x

A2 x O

A3

A4

x O x

O

x x

B

C

D

E

O O x

O x

O

b Figure 1.4. Influence de la granularité En conclusion, le sens de la complexité d’un système varie selon le contexte dans lequel le système est vu et ce qu’on cherche à décrire de ce système. Pour comprendre réellement un

système, nous devant choisir une granularité significative. Une granularité forte peut laisser croire que le système est très simple et une granularité fine engendre beaucoup d’information lors de la description. Gérer la complexité d’un système dépend, en partie, de notre capacité à améliorer sa compréhensibilité. Même si cela ne réduit pas réellement la complexité, il améliore, cependant, notre capacité à comprendre et à raisonner sur le système.

1.3 Comment résoudre les problèmes de la complexité ? Lorsque la complexité n’est pas bien gérée, elle engendre diverses difficultés qui entraînent des conséquences plus ou moins graves : -

Produits de qualité médiocre

-

Livraison au delà des délais

-

Dépassement du budget alloué

-

Abondant du produit

Ces conséquences découlent directement ou indirectement de notre incapacité à cerner les dépendances multiples qui existe dans un système donné. Souvent, on sous-estime l’effort nécessaire au développement et, plus tard, à l’évolution car on n’arrive pas à cerner l’impact qu’a un changement quelconque sur l’ensemble du système. La présence des outils d’analyse de l’impact peut être très utile si nous disposons d’une bonne description du système. L’analyse manuelle de l’impact est possible mais nécessite une bonne compréhension du système préalablement à toute estimation. Cependant, face aux systèmes complexes, il n’est pas aisé d’arriver à une bonne estimation. Il n’y a pas de solutions faciles qui puissent éliminer la complexité et augmenter la productivité et la qualité. Le mieux qu’on puisse faire est de réduire ou gérer la complexité. A l’origine, la complexité découle de la présence de dépendances entre les entités. Pour que la complexité devienne visible, il faut d’abord percevoir le système sous forme d’entités (i.e. utiliser une granularité significative). Vu comme un seul bloc, le système ne révèle aucune complexité. Pour construire ce système, il faut imaginer une certaine décomposition en parties de telle sorte à avoir un minimum de dépendances entre ces parties. La construction s’opérera partie par partie en commençant par la partie qui ne présente aucune dépendance avec les autres parties. L’idéal dans ce processus de construction étant d’avoir des parties totalement indépendantes. Deux notions étroitement liées peuvent être utilisées pour gérer la complexité. L’abstraction est la modularisation. L’abstraction signifie cacher les détails d’un niveau donné alors que la modularisation fait référence à la décomposition en partie ou modules. Dans un premier temps et selon un processus descendant, il est possible de décomposer le système qu’on souhaite réaliser (ou comprendre) en modules. Chaque module à des dépendances avec d’autres modules et est lui-même composé de sous-modules entre les quels existe d’autres

dépendances, et ainsi de suite. On peut à tout moment raisonner sur un module sans intéresse à sa structure interne, celle-ci reste cachée et on parle alors d’abstraction. Le module est une abstraction de certaines parties (fonctionnalités) du système. On peut réitérer la modularisation et l’abstraction à un niveau de granularité faible et on s’arrêtera lorsque les modules obtenus présentent une complexité simple qui n’entrave pas leur réalisation. Bien que les notions d’abstraction et de modularisation soient simples, leur mise en œuvre n’est pas aisée. En effet, face à un système donné, comment détermine-t-on le nombre de modules, leurs structures, leurs dépendances internes est externes ? Quelles sont les natures des modules et des dépendances ? Concernant, les dépendances, un principe important doit être appliqué : Un module doit avoir un minimum de dépendances avec les autres modules (on parle de couplage faible) et un maximum de dépendances internes entre ces constituants (on parle de cohésion forte). Les dépendances peuvent avoir divers types. Ainsi, si on décompose un système en classes, on peut avoir des dépendances hiérarchiques entre classes et sous classes, des dépendances d’utilisation si une classe utilise une opération décrite dans une autre, des relations diverses (telle que agrégation et composition) qu’une classe matérialise par des attributs de liens sur d’autres classes, etc. Les modules peuvent être aussi de divers types : des composants ayant des interfaces publiques, des classes, des patrons, des aspects, … . Nous présenterons ces différentes possibilités dans les chapitres suivants. La modularité et l’abstraction concernent à la fois le système en tant que produit et sa construction/modification. Ces dernières font références au processus de développement et d’évolution. Concernant le processus, lorsqu’un élément B dépend d’un élément A, la réalisation d’une certaine tâche (conception ou modification) sur A doit précéder la réalisation de cette tâche sur B. Si A et B sont interdépendants, alors le changement de l’un requière le changement de l’autre de manière itérative. Ainsi, la complexité au niveau du produit lui correspond une complexité relative au niveau du processus de conception/modification. Les systèmes complexes peuvent être représenté par un graphe où les nœuds correspondent aux éléments ou aux tâches et les arcs dirigés correspondent aux dépendances. Lorsqu’il s’agit d’interdépendance, le graphe devient cyclique (voir figure 1.5). Du point de vue réalisation des tâches, il est important de briser ce cycle ou empêcher sans existence.

A

B

Figure 1.5: Interdépendance entre les tâches A et B

Un arc de A à B indique que B dépend de A ou suit A (autrement dit le flux d’information de conception ou modification va de A vers B). Un graphe ayant N noeuds peut avoir au plus N2-1 dépendance. Avec l’augmentation de N, le nombre de dépendances potentielles augmente de manière exponentielle. Dans un système à 4 nœuds, il y a potentiellement 12 dépendances Si on considère la décomposition d’un système en classe, le graphe précédent peut représenter une hiérarchie de classes ou la relation de dépendance et de type généralisation/spécialisation. Le problème de cycle est écarté car en terme de généralisation/spécialisation, il n’a aucun sens. Avec N classes, on ne peut avoir que N-1 relations d’héritage donc la complexité est réduite et on peut du point de vue tâche commencer par la classe la plus générale puis aller vers les classes les plus spécifiques. Si on considère d’autres types de relations entre les classes (utilisation ou agrégation), la complexité augmente de façon sensible car entre deux classes il peut y avoir plusieurs formes de dépendances. Il existe actuellement plusieurs approches qui permettent de gérer la complexité. Les points communs entre ces approches résident dans la modularisation et l’abstraction. Ces différentes approches peuvent être partagé en approche produit et approche processus.

1.3.1 Les approches basées processus Nous regroupons, sous cette appellation, les approches qui ciblent directement le processus de développement et de maintenance. On distingue principalement trois approches : la stabilité/variabilité, la réutilisation et l’ingénierie des modèles. Alors que l’ingénierie des modèles provoque actuellement un changement profond dans le processus de développement et de maintenance, la stabilité/variabilité et la réutilisation n’occasionnent qu’un changement limité. Dans ce qui suit, nous décrivons brièvement les trois approches basées processus.

La stabilité et la variabilité La stabilité logicielle est une approche qui cherche à identifier et à séparer les parties stables d’un système des parties susceptibles de changer. Selon Fayad, parvenir à la stabilité se fait en cherchant à séparer les classes d’un système selon les trois couches suivantes [Fay 01] : ›

La couche des thèmes métiers durables (Enduring Business Themes ou EBTs) : Elle contient les classes qui représentent des connaissances de base du domaine en question. De ce fait, elles sont extrêmement durables et forment le noyau du modèle de stabilité logicielle que l’auteur propose. Deux caractéristiques distinguent les EBTs des autres classes. La première est le fait qu’elles soient conceptuelles et non palpables (i.e. représentent des concepts). La deuxième est que les classes EBTs soient communes à toutes les applications du domaine où elles ont été identifiées.

›

La couche objets métiers (Business Objects) : Elle contient les classes qui sont des projections des EBTs sous une forme plus concrète. Les objets métiers sont plus palpables et stables de l’extérieur mais intérieurement adaptables. Les caractéristiques qui différencient ces classes sont au nombre de trois. La première

est le fait qu’elles soient stables de l’extérieur mais intérieurement adaptables. La deuxième est qu’elles correspondent à des objets semi concrets et la troisième est le fait qu’elles soient communes (extérieurement) entre toutes les applications où elles ont été identifiées. ›

La couche objets industriels (IOs) : Contenant les classes des objets physiques qui sont des projections des objets métiers. Les objets industriels se caractérisent par trois propriétés. Ils sont instables et peuvent être remplacés, ajoutés ou supprimés d’un système, sans affecter le noyau de ce dernier. Les objets industriels sont concrets et palpables dans le sens où ils représentent souvent des objets du monde réel. La dernière propriété est le fait qu’ils peuvent être totalement différents d’une application à l’autre dans le même domaine.

Parvenir à la stabilité, durant le développement d’un système, améliore considérablement la qualité et réduit les coûts du développement du produit. Cependant, l’identification de EBTs et objets métiers d’un domaine n’est pas une tâche facile, même pour ceux qui ont une longue expérience dans ce domaine. Malgré que la stabilité a existé depuis longtemps, il n’y a pas encore de méthodologie qui en tienne compte. L’apport de la stabilité pour le processus de développement/changement est indéniable. En effet, les objets abstraits ne sont pas sujets aux changements comme c’est le cas des objets physiques. En d’autres termes, l’approche préserve une partie du système des changements futurs. Par ailleurs, la variabilité représente un vaste domaine qui s’intéresse à la gestion, durant le processus de développement, des éléments qui diffèrent d’un produit à l’autre dans une famille de produits logiciels (on parle de lignes de produits) et parfois au sein d’un même produit [Gur 01]. La variabilité est concernée aussi bien par le processus de développement lui-même que par les éléments (artefacts) qu’il crée, tels que les spécifications des besoins, les documents de conception, le code exécutable, ... Gurp considère que la variabilité au niveau des logiciels (ce qui diffère d’un produit à l’autre) est en train de croître considérablement [Gur 01]. La première raison à cela est le fait que la variabilité, qui était jadis au niveau matériel des systèmes, est en train de passer vers les logiciels embarqués qui les pilotent (le système devient alors stable au niveau matériel, mais présente une variabilité au niveau logiciel). Par exemple, plusieurs constructeurs de moteurs de véhicules proposent des versions de moteurs qui ne diffèrent que par leurs contrôleurs. La deuxième raison est le fait de retarder les décisions de conception jusqu’au dernier niveau possible dans le cycle de vie. Ce retard est intéressant du point de vue économique et du point de vue de l'augmentation de la configurabilité du produit [Jaz 00]. Introduire la variabilité dans une ligne de produits n’est pas une tâche aisée car plusieurs facteurs influencent la manière avec laquelle une décision peut être retardée. Parmi ces facteurs, on trouve la taille du logiciel, la plateforme d’exécution du futur produit, la limite à laquelle il est possible de retarder une décision de conception donnée, etc. La variabilité se base sur un ensemble de concepts que nous présentons ci-après. Caractéristique. C’est une unité logique de comportement qui est spécifiée par un ensemble de besoins fonctionnels et de qualité. On peut la considérer comme une forme d’abstraction des besoins. Chaque abstraction peut correspondre à plusieurs besoins, et inversement. Une ligne de produits fournit une architecture centrale qui peut être changée et spécialisée en produits concrets. Cette spécialisation se fait en ajoutant des

caractéristiques et en changeant d’autres. Les caractéristiques peuvent être de plusieurs types vis-à-vis du produit en cours de développement [Gur 01]: ›

Les caractéristiques obligatoires. Par exemple, pour un éditeur de textes, une caractéristique obligatoire est celle de permettre la saisie d’un texte.

›

Les caractéristiques optionnelles. Ce sont les caractéristiques qui améliorent le produit sans être nécessaires. Par exemple, la détection et correction des fautes d’orthographes pour un éditeur.

›

Caractéristiques variantes. Une caractéristique variante (ou simplement variante) est une abstraction d’un ensemble de caractéristiques reliées.

›

Caractéristiques externes. Ce sont les caractéristiques apportées par la plateforme pour laquelle le système est destiné. Ces caractéristiques ne font pas partie du système, mais il en dépend et les utilise. Par exemple, une plateforme peut mettre à la disposition d’un éditeur de textes des polices de caractères ou un type de fenêtre d’affichage, ... . C'est la différence entre diverses plateformes qui nécessite d’inclure une partie des caractéristiques externes dans le logiciel, pour qu’il puisse lui-même gérer la variabilité.

Les points de variation. Ils indiquent les emplacements d’un logiciel où un choix est effectué pour l’utilisation d’une variante donnée. La variabilité, du point de vue tâches à inclure dans le processus de développement, se base sur les quatre points suivants [Gur 01]: ›

Identifier la variabilité. En premier lieu, il s’agit d’identifier où la variabilité est nécessaire. Identifier la variabilité n’est pas une chose facile, cependant, si on dispose d’une représentation d’un système sous forme de caractéristiques, elle devient plus simple à réaliser.

›

Poser des contraintes sur la variabilité. Il s’agit de décider quand une variante doit être introduite dans le produit ou dans le processus de développement, quand et comment une variante est ajoutée au système, quel est le moment d’établissement de la liaison entre point de variation et variante. Une fois les variantes identifiées, elles sont conçues sous forme d’une ou plusieurs entités logicielles. Puis les emplacements dans le système sont identifiés (ce qui correspond aux points de variation) où les entités de la variante sont reliées avec le reste du système.

›

Implémenter la variabilité. Il s’agit de trouver un compromis entre les différentes contraintes présentées précédemment. L’implémentation peut être une réorganisation architecturale du produit, un changement partiel de l’architecture (un composant de celle-ci), l’ajout d’un composant au moment de la compilation ou au moment de l’exécution, l’utilisation de librairies, etc.

›

Gérer la variabilité. Il s’agit de décider, sur une période de temps donnée, du maintien ou non des variantes, de l’ajout de nouvelles variantes dans la ligne du produit ainsi que l’évaluation de l’intérêt de maintenir ou d’abandonner les variantes.

La réutilisation La réutilisation part d’un principe simple : au lieu de reconstruire un nouveau système logiciel dans sa totalité, il serait bénéfique de tirer profit de la similarité entre les différents systèmes, de réutiliser les parties semblables et reconstruire les parties différentes. La réutilisation se base sur le fait que les études comparatives révèlent que les logiciels sont

similaires de 60% à 70% et que cette similarité existe à différents niveaux d’abstraction et peut être encore plus importante lorsqu’il s’agit d’applications d’un même domaine [Mcc 97]. La réutilisation peut être définie comme l’utilisation répétée d’un élément (artifact) en dehors de son contexte original pour construire de nouveaux systèmes [Fra 95]. Par élément, on sous entend tout ce qui peut être produit par les phases de développement. Ceci inclut la documentation, les différents modèles de conception, les prototypes, les programmes, etc. La réutilisation diffère de l’utilisation qui, elle, intervient pendant l’exécution et a un sens d’un appel de programme [Bas 96]. La réutilisation vise divers objectifs : augmenter la productivité durant la phase de développement, réduire les délais, améliorer la qualité, augmenter la fiabilité, réduire les coûts, etc. Même si l’idée de la réutilisation est simple, sa mise en oeuvre n’est pas triviale. En effet, elle nécessite des changements au niveau du processus de développement, des outils de développement et des habitudes relatives aux intervenants. L’extension du processus de développement peut être effectuée de différentes façons, nous citons les quelques points suivants [Mcc 97]: ›

Penser à la réutilisation dans chaque phase du développement.

›

Prévoir, dans la phase d’analyse, une sous phase qui identifie les éléments pouvant être réutilisés.

›

Inclure des options spécifiques à la réutilisation dans les outils et méthodes utilisés.

›

Inclure, dans la documentation, des informations sur la réutilisation.

La réutilisation exige certaines conditions pratiques pour donner des fruits : ›

Avoir un élément suffisamment important pour justifier la réutilisation (e.g. classes, cas de test, architecture).

›

L’élément doit être bien documenté, facile à comprendre, d’une bonne qualité et facile à modifier.

›

Existence d’une librairie de réutilisation qui stocke, gère les éléments réutilisables tout en facilitant la recherche et l’accès à ces derniers.

Les architectures basées modèles L’ingénierie dirigée par les modèles (MDE pour Model-Driven Engineering) est une nouvelle approche en pleine expansion qui suscite un intérêt grandissant dans la communauté génie logiciel [Béz 04]. A l’origine de cette approche, on trouve la proposition, nommée MDA (pour Model Driven Architecture) faite par l’OMG (Object Management Group) en novembre 2000 [OMG 04]. Il s’agissait alors de se baser sur UML pour rendre la description des systèmes indépendante des plateformes, conduisant ainsi à une meilleure portabilité, interopérabilité et réutilisation. Dans l’approche MDA, les systèmes sont décrits moyennant des modèles qui peuvent être de deux types : des modèles indépendants des plateformes dits PIM (Platform Independent Model) et des modèles liés aux plateformes dits PSM (Platform Specific Model). Un PIM contient les parties d’un système qui ne changent pas d’une plateforme à l’autre. Initialement, un système est décrit dans un langage indépendant des plateformes, tel que UML, pour obtenir un PIM. Par

la suite, pour obtenir un système opérationnel, le PIM est transformé en PSM en effectuant un mapping vers un langage qui soit spécifique à une plateforme donnée [Mel 04, Mil 01, Mil 03, Poo 01]. L’approche est simple et se base sur un ensemble de standards largement acceptés par la communauté scientifique, on cite : UML, MOF (Meta Object Facility), XML-XMI (XML Metadata Interchange) et CWM (Common Warehouse Metamodel). Même si l’interopérabilité est placée comme le cheval de bataille de cette approche, d’autres retombées sont nombreuses et importantes au point où, maintenant, il se dégage un courant de généralisation qui considère qu’un logiciel n’est qu’un ensemble de modèles donnant chacun une facette de ce dernier, et que les activités liées à l’ingénierie des logiciels et leur maintenance ne sont en fait que des transformations de modèles [Béz 04]. Il résulte de ce courant de généralisation, des perspectives de recherche prometteuses qu’on peut classer dans les catégories suivantes : ›

La modélisation. Il s’agit d’abstraire un système réel étudié pour en ressortir les éléments essentiels permettant de simplifier la communication, la compréhension et le raisonnement, et, conduire ainsi à un objectif donné. La modélisation continue à être un créneau porteur car les paradigmes ne cessent d’évoluer. Cette évolution doit se faire en adéquation avec l’approche MDE. Par exemple, quelle est la position de l’approche basée composants? Où doit on tenir compte de celle-ci durant la modélisation ? … Notons que dans la MDE, tout modèle est conforme à un métamodèle et que ce dernier a une forte influence sur notre façon de modéliser une réalité donnée.

›

La validation des modèles. Le modèle étant une base pour la prise de décision, il doit être sain syntaxiquement et sémantiquement. Plusieurs travaux relèvent de la validation, il s’agit par exemple de l’identification des critères de qualité d’un modèle, de vérification de la cohérence interne d’un modèle, de vérification de la conformité des modèles aux métamodèles, de l’étude du comportement opérationnel, …

›

La définition des métamodèles. De la qualité du métamodèle dépendra la qualité du modèle. Dans ce domaine, il s’agit de trouver des métamodèles qui soient à la fois riches et précis et dont la sémantique se sépare clairement de la syntaxe.

›

La transformation de modèles. Elle revêt un caractère décisif, car la MDE ambitionne l’automatisation du passage d’un modèle à un autre. Cependant, le chemin est long pour y parvenir, car on constate actuellement une grande diversité des méta modèles qui va à l’encontre de l’approche de généralisation de la MDE. Dans ce domaine, beaucoup de travail reste à faire concernant la comparaison de méta modèles et modèles, la transformation par raffinement, la composition des modèles, etc.

Les MDA offrent un contexte de développement basé sur les modèles des systèmes logiciels. L’accent mis sur les modèles permet d’élever le niveau d’abstraction et, particulièrement, de découpler les PIM des PSM en mettant l’accent, dans les premiers, sur les fonctionnalités métiers et règles de gestion [Mil 01, Mil 03]. Un large éventail d’applications visées par l’approche seront de ce fait indépendantes des plateformes et pourront tourner sur diverses plateformes : .Net, J2EE [J2e 04], ou autre (figure 1.6).

Télécommunications Transport

Finances

MDA

Commerce Electronique

Applications Médicales

Fabrication Industrielle

Applications Spatiales Autre …

Figure 1.6. Vision de l’approche MDA L’approche MDA se base sur un ensemble de standards, incluant UML, MOF, CWM et XMI/XML, qui permettent la construction et la gestion des modèles. UML (Unified Modeling Language) est une spécification définissant un langage graphique pour la visualisation, la spécification, la construction et la documentation de tous les éléments (artifacts) des systèmes logiciels. En particulier, il peut être utilisé pour la conception des PIM. MOF (Meta Object Facility) est un cadre extensible d’intégration basée modèles pour la définition, la manipulation et l’intégration des métadonnées et données. Il est considéré comme un méta métamodèle. MOF fournit le standard de méta-modélisation et d’échange de constructions utilisées en MDA. UML, CWM et le mécanisme par lequel les modèles sont sérialisés en XMI sont définis par des constructions MOF. D’une façon générale, il définit les éléments essentiels, la syntaxe et la structure des méta-modèles utilisés pour construire des modèles orientés objets. Le langage XMI (XML Metadata Interchange) permet de décrire une instance du MOF sous forme textuelle, en utilisant le langage XML (eXtensible Markup Language). Les métamodèles MOF sont décrits par des DTDs XML (DTD pour Document Type Definitions) et les modèles traduits dans des documents XML conformes à leur DTD correspondante. XMI résout les problèmes de représentation textuelle des objets et leurs associations. Le CWM (Common Warehouse Metamodel) définit des métamodèles qui : ›

Permettent la représentation des métadonnées aussi bien métiers que techniques, qui sont le plus souvent trouvées dans les entrepôts de données (Warehouse).

›

Facilitent l’échange des entrepôts de données et des métadonnées entre les outils des entrepôts de données, les plateformes supports et les répertoires des métadonnées des environnements distribués hétérogènes.

Ainsi, le CWM définit des métamodèles de représentation des schémas, leur modèle de transformation et les modèles de fouille des données. Le processus MDA se compose de trois grandes étapes : construction du PIM, transformation en PSM, puis transformation en code [Kla 04]. 1- Création d’un PIM. Le processus de développement commence par la création d’un PIM ayant un haut niveau d’abstraction, ne dépendant d’aucune technologie d’implémentation particulière. La description utilise un langage, comme UML, capable de décrire les divers éléments (artifacts) avec leurs comportements et leurs contraintes. Dans l’approche MDA, on fait appel à plusieurs PIMs qui sont créés par des transformations en cascade (i.e. structurés en pile). Le PIM de plus bas niveau (i.e. le dernier créé de la pile) exprime uniquement les comportements et les fonctionnalités métiers. Dans un premier temps, les experts du domaine considéré et les experts en création de modèles essayent d’extraire les règles de gestion et les fonctionnalités métiers avec, autant que possible, une indépendance des technologies d’implémentation. En particulier, les détails spécifiques aux plateformes sont absents (e.g. persistance, gestion des transactions, sécurité, configuration). En ajoutant ces derniers aux PIMs de second niveau, il serait plus simple d’avoir des PSM précis à l’étape suivante. Au niveau de la première étape, le PIM obtenu spécifie les fonctionnalités et le comportement du logiciel, en effet : ›

Les diagrammes produits matérialisent la structure (i.e. classe et objets),

›

Le diagramme d’activité matérialise le comportement,

›

Les classes et objets augmentés de notations sémantiques incluent des facteurs de gestion et

›

D'autres aspects du modèle matérialisent plateformes.

les aspects indépendants des

2 – Création du PSM. Le PIM de l’étape précédente représente une entrée de cette étape qui l’utilise pour produire un PSM. Le PSM est un modèle façonné par des constructions disponibles dans une plateforme donnée. Le PSM peut être décrit en utilisant les diagrammes UML, ou moyennant des définitions d’interface dans une technologie donnée (XML, Java, Corba IDL, …). Dans les deux cas, les contraintes sont spécifiées en utilisant UML ou en langage naturel. Si le logiciel se compose de plusieurs modules, il se peut que chacun soit généré en utilisant une plateforme différente. La transformation du PIM en PSM peut s’exprimer de deux façons différentes : mapping des types et mapping des instances; cependant la transformation peut combiner les deux. Dans le mapping des types, tous les types qui apparaissent dans le PIM sont transformés en types du modèle PSM. Cette catégorie de transformation inclut le mapping des métamodèles. Dans ce cas, on décrit la transformation de façon générale entre deux métamodèles, chacun étant spécifié comme métamodèle MOF. Dans le mapping des instances, on identifie, par un marquage, les éléments d’un modèle PIM qui devraient être transformés en considérant une plateforme donnée pour le PSM (un élément peut être marqué plusieurs fois). Par exemple, si un élément d’un PIM est marqué comme étant une entité, la transformation peut consister à le transformer en classe Java, ou un autre type d’entités PSM. Une marque peut aussi consister

en une qualité de service qui sera traduite de façon appropriée dans le PSM. Les transformations peuvent être effectuées automatiquement ou manuellement. Siegel identifie dans [Sie 01], quatre possibilités avec la dernière comme l’ultime objectif des MDA : ›

Une transformation totalement manuelle.

›

Une transformation manuelle en appliquant les patrons de conversions du PIM vers un PSM particulier.

›

Les patrons peuvent être des algorithmes supportés par des outils automatiques produisant un squelette de PSM qui est alors complété manuellement.

›

Un outil automatique permettant d'appliquer un algorithme et générer le PSM dans sa totalité.

3 – Génération du code. Partant d’un ou de plusieurs PSM, on génère une implémentation pour un plateforme donnée, moyennant un outil de transformation. L’implémentation inclut aussi les spécifications de déploiement. Le processus MDA n’est pas totalement différent du développement classique en génie logiciel [Kle 03]. Cependant, hormis le fait qu’on retrouve les mêmes étapes, ce que produisent ces étapes est différent. L’approche de développement classique consiste principalement en une succession d’étapes qui va de l’étude des besoins au test et déploiement (figure 1.7). Ce que produisent les étapes d’analyse et de conception (i.e. texte et diagrammes) est différent de ce que produisent les mêmes étapes en MDA (PIM et PSM). Les transformations jouent un rôle central dans les processus MDA. Ils permettent la conversion d’un modèle en un autre moyennant des outils qui appliquent une définition de la transformation donnée sous forme de règles. L’approche MDA ambitionne une automatisation importante du processus de développement

Besoins

Besoins Texte en majorité

Texte en majorité Analyse

Diagrammes et texte

Diagrammes et texte Codage Code Test

PIM

Réitération (En théorie)

Conception Détaillée

Raccourci de programmation

Réitération (En théorie)

Analyse

Conception Détaillée PSM Codage Code Test

Code

Code

Déploiement

Déploiement

Cycle de vie classique

Cycle de vie en MDA

Figure 1.7. MDA et cycle de vie

Dans la figure 1.8, nous donnons un métamodèle, tiré de [Mub 04] qui décrit le processus MDA en faisant ressortir les relations entre PIM, PSM et les autres éléments. UML



1..n

1..n



PIM

1..n

1..n 1..n

Métamodèle



Langage ...









1..n PSM



MOF

Techniques de translation PIM



Plateforme

Techniques de translation PSM 1..n

Figure 1.8. Le métamodèle du processus MDA

1.3.2 Les approches basées produit Sous cette appellation, nous regroupons les approches qui ciblent en premier lieu le système logiciel final. Nous considérerons dans ce document trois approches principales que sont : la séparation avancée des préoccupations, les composants logiciels et les patrons de conception. Ces trois approches seront décrites dans les chapitres suivants. Notons en fin de ce chapitre que, même si nous avons opérer une séparation en approches basées produits et approches basées processus, il faut noter l’existence d’une dualité processus-produit, dans le sens où l'intervention sur l'un engendre un impact sur l'autre et réciproquement. L'expérience nous a montré, qu'une réelle avancée en génie logiciel ne peut s'opérer que lorsque une approche ciblant une amélioration des processus atteint aussi le produit et inversement. L'approche orientée objet en est un exemple. Le langage SmallTalk (du PARC de Xerox) fût le premier à proposer (en 1979) un modèle orienté objets clairement et consciemment défini. Il s'agissait alors de concepts simples tels que l'encapsulation, l'envoi de message, le polymorphisme et l'héritage. Malgré que les scientifiques admettaient l'intérêt de l'approche orientée objet, il a fallu attendre l'avènement des méthodes telle que OMT, qui mettaient en avant les concepts de l'approche dans le processus de développement. Le tournant décisif fût atteint lorsque le consensus sur UML vint le jour.

2 LA SEPARATION AVANCEE DES PREOCCUPATIONS Sous l’appellation séparation avancée des préoccupations, nous regroupons un ensembles de nouvelles approches qui partagent un point commun: celui de considérer qu’un système a des aspects fonctionnels et des aspects non fonctionnels. Ces derniers sont appelés préoccupations (concerns). L’aspect fonctionnel fait référence aux tâches de base que le système réalise et pour lesquelles il a été conçu. Quant aux préoccupations, les exemples les plus couramment cités traitent de la synchronisation, la sécurité, la gestion des exceptions, la gestion de la persistance, la gestion des transactions, l’optimisation, etc. Les approches de séparation des préoccupations prônent la séparations des préoccupations des parties fonctionnelles comme une approche qui permet une meilleure modélisation et ayant une influence bénéfique sur les processus de développement et de maintenance. Actuellement, il est communément admis qu’une bonne séparation des préoccupations réduit la complexité des systèmes logiciels et l’enchevêtrement de leur code, facilite la réutilisation, améliore la compréhension, simplifie l’intégration de composants et décroît les changements en cascade [Aks 98, Aks 98a, Kic 97, Oss 99]. Dans sa forme la plus générale, la séparation est multidimensionnelle dans le sens où le système logiciel n’est pas partagé en partie fonctionnelle et partie non fonctionnelle, mais la séparation peut être faite simultanément selon des dimensions multiples et de type arbitraire. Par conséquent, les préoccupations elles mêmes interagissent et se recouvrent [Oss 99, Tar 99]. De nos jours, une large part des travaux sur la séparation traite de trois principales approches : l’approche orientée aspect [Kic 97], l’approche composition de filtres [Aks 98] et l’approche de séparation multidimensionnelle [Oss 99]. Bien que les auteurs de ces approches reconnaissent l’intérêt de séparer les préoccupations importantes d’un logiciel, ils ne s’entendent pas sur les concepts et mécanismes à utiliser. La philosophie de chaque approche est différente et les concepts utilisés sont variés et ad hoc [Mes 04a]. Dans la suite, nous présentons ces trois approches.

2.1 La programmation orientée aspects Principe et objectifs. Les auteurs qui sont à l’origine de cette approche, Kiczales et ses collègues, ont pris comme point de départ deux inconvénients dans le modèle orienté objets classique : l’enchevêtrement et l’éparpillement de code, et prônent la séparation des

préoccupations comme un moyen d’éliminer ces inconvénients [Kic 97]. En effet, si on considère la sécurité dans Java, on constate que chaque classe du code fonctionnel contiendra une partie du code relative à cette préoccupation, ce qui constitue un éparpillement du code de cette dernière. De même, si on considère une classe donnée, elle renfermera le code fonctionnel mélangé avec une multitude de parties de code représentant les diverses préoccupations impliquant la classe. On parle alors d’enchevêtrement de code. Les initiateurs de la programmation orientée aspects considèrent que la séparation des préoccupations élimine l’enchevêtrement et l’éparpillement de code, de plus, ils énoncent un critère important pour caractériser les préoccupations, celui d’être entrecoupantes pour les classes fonctionnelles (en anglais, le terme crosscutting est souvent utilisé). Dans cette approche, les préoccupations sont appelées Aspects. Lors de la conception ou de la programmation, toute préoccupation qui entrecoupe plusieurs classes est considérée comme un aspect [Kic 01]. Sur le plan pratique, les supports d’exécution n’ayant pas changés, un programme orienté aspects ne peut être directement exécuté. Pour cela les auteurs proposent une nouvelle forme de transformation dite tissage (Weaving). Ce dernier ressemble à une compilation et a pour rôle d’intégrer les aspects et les classes pour produire un code exécutable. Même si cela signifie une régénération de l’enchevêtrement et l’éparpillement, le tissage n’élimine pas les avantages de l’approche car le code source demeure un code orienté aspects. De ce qui précède, la vision motivante de l’approche orientée aspects est que lors de la conception/programmation, on peut fournir des spécifications indépendantes pour chaque préoccupation et chaque fonctionnalité, puis effectuer leur tissage pour produire le système final. L’approche orientée aspects est appropriée pour divers type d’applications. Elle présente des avantages indéniables dans toute application où la sécurité, la synchronisation, la gestion des transactions, la répartition, ..., sont considérées comme des aspects et sont séparées de la partie fonctionnelle du système. L’approche orientée aspects offre une meilleure modélisation des applications par le fait que le code est réduit, moins enchevêtré et plus proche de notre perception du monde réel. Par conséquent, l’approche améliore la réutilisation, l’évolution et la maintenance. L’approche orientée aspects existe actuellement sous forme d’extension à des langages tel que Java ou C++ et est utilisée principalement au niveau programmation et, à un degré moindre, à des niveaux plus abstraits. Dans la section suivante nous présentons les concepts de base en utilisant comme langage orienté aspects AspectJ, ce dernier étant un précurseur. Concepts de base. AspectJ est une extension, à usages multiples, de Java. Elle utilise deux approches pour modéliser les entités entrecoupantes. On parle d’entrecoupage dynamique et d’entrecoupage statique. Cette appellation est liée au moment de la mise en application de l’une et de l’autre. Alors que l’entrecoupage dynamique intervient à l’exécution du code fonctionnel, l’entrecoupage statique intervient au moment du tissage. Dans l’entrecoupage dynamique trois concepts sont utilisés : point de jointure, point de coupure et consigne. L’aspect est une entité qui ressemble à une classe, mais modélise une préoccupation entrecoupante. Pour comprendre le principe de fonctionnement d’AspectJ, considérons un programme en exécution. A tout moment, le système passe d’un état à un autre et génère des faits et des évènements tels que l’accès ou la modification du champ d’un objet, l’appel d’une méthode, son exécution et le retour de valeurs, etc. Tous ces points observables sont appelés points de jointure (join points) et représentent des points d’interruptions où

l’aspect peut intervenir. Tout se passe comme si l’exécution du code fonctionnel est interrompue aux points de jointure pour laisser la possibilité au code de l’aspect de s’exécuter et réaliser le but de la préoccupation qu’il implémente. Parmi tous les points de jointure, seul un nombre réduit peut intéresser un aspect à un moment donné. Le code source de ce dernier, spécifie les points de jointures qui lui sont significatifs en utilisant des points de coupure (pointcuts). Les points de coupure sont des formes de prédicats qui utilisent des opérateurs booléens et des primitives spécifiques pour capturer les points de jointures et les informations concernant leur contexte dynamique. Par exemple, le point de coupure peut spécifier l’exécution d’une méthode et les paramètres de l’appel de celle-ci. Plusieurs aspects peuvent exister à un moment donné dans un système et être concernés par les mêmes points de jointure et les mêmes points de coupure. Dans ce cas, les aspects doivent être composés selon certaines règles de précédence. Le code de l’aspect est divisé en plusieurs blocs dits consigne (advice). Ces blocs ressemblent aux méthodes Java et sont utilisés pour déclarer un code qui doit s’exécuter une fois que les points de jointure d’un point de coupure sont atteints. Il existe trois types de relation entre points de coupures et consignes. Ces types déterminent aussi le type des consignes : before, after et around (qu’on peut traduire par avant, après et autour). Si un point de coupure est un appel à une méthode m(), les aspects concernés doivent contenir un point de coupure qui spécifie le point de jointure appel de méthode et une consigne qui s’exécutera avant ou après l’appel de m() ou même autour de cet appel (avant et après). Ce dernier cas signifie que la consigne peut remplacer l’appel de la méthode comme elle peut ajouter un traitement à faire avant l’appel et un autre après l’appel. Pour cette dernière situation, une instruction spéciale appelée proceed() disponible uniquement à l’intérieur d’une consigne around peut relancer l’exécution du traitement associé aux points de jointure. Les figures 2.1 de a à d, montrent comment une consigne, en fonction de son type, agit sur le flot d’exécution normal d’une méthode. ASPECTJ dispose d’un ensemble de points de coupure primitifs très riche, permettant une spécification variée des points de jointures dans un aspect. La table 2.1 résume la signification prédicative de ces points de coupure et leur composition. La puissance des consignes réside dans le fait qu’elles peuvent accéder aux informations contextuelles dynamiques capturées par les points de jointure. On appelle cette possibilité l’exposition du contexte (context exposure). Par exemple, les points de coupure peuvent capturer les paramètres et l’objet cible d’un appel. Pour cela, on peut spécifier, avec les points de coupure et les consignes, des paramètres qui seront liés dynamiquement aux valeurs du contexte des points de jointure courants. De plus, les points de coupure peuvent avoir une puissance descriptive importante, en utilisant le joker ‘*’ à la place d’un type, d’une méthode, d’un paramètre ou comme partie d’un nom. De même, ‘..’ est utilisé pour spécifier un ou plusieurs paramètres et ‘+’ pour inclure les sous-classes d’une classe donnée. L’entrecoupage statique modifie un programme au moment du tissage. AspectJ offre des constructions qui permettent de déclarer, dans un aspect, de nouveaux membres qui seront insérés dans les classes spécifiées. Il permet aussi de modifier ce qu’une classe étend ou implémente (héritage et implémentation d’interface). Lorsqu’il s’agit de nouveau membres, on parle d’introduction ou de déclaration inter-type de membre. Les

introductions spécifient les classes concernées en préfixant, dans l’aspect, le ou les membres par le nom de la classe où ils seront insérés. Si ce préfixe est absent, le membre appartient à l’aspect.

Object o1 … method m(…) {… o2.m’(…) … … }

Appel et retour

Object o2 … method m’(…) {… … … }

n

o

Capture de l’appel ou du retour Appel ou retour possible Appel ou retour précédent n…r Ordre chronologique

Figure 2.1–a. Code fonctionnel sans aspects

Object o1

Object o2

… method m(…) {… o2.m’(…) … … }

… method m’(…) {… … … }

n q

Aspect a1 o

p

… Pointcut p() … Before p() … … …

Figure 2.1-b. Code fonctionnel avec un aspect utilisant une consigne avant

Object o1

Object o2

Aspect a1

… method m(…) {… o2.m’(…) … … }

… method m’(…) {… … … }

… Pointcut p() … After p() … … …

n

o

p

q

Figure 2.1-c. Code fonctionnel avec un aspect utilisant une consigne après

Object o1

Object o2

… method m(…) {… o2.m’(…) … … }

… method m’(…) {… … … }

n

r

Aspect a1 o p q

… Pointcut p() … around p() … proceed() …

Figure 2.1-d. Code fonctionnel avec un aspect utilisant une consigne autour

Point de coupure

La valeur du prédicat correspondant Commentaires (cjp signifie point de jointure courant)

call(S)

Vrai si cjp correspond à un appel de S

execution(S)

Vrai si cjp correspond à une exécution de S

get(S)

Vrai si cjp correspond à un accès à S

set(S)

Vrai si cjp correspond à une affectation de S

initialization(S)

preinitialization(S)

S ::=ResultType ClassName.MethodName (Parameters) pour spécifier une méthode ClassName(Parameters) pour spécifier un constructeur S ::= ClassName.FieldName

Type

Vrai si cjp correspond à l’exécution de l’initialisation S ::= ClassName(Parameters) d’un objet de S Parameters sont les paramètres premier constructeur. Vrai si cjp correspond à l’exécution de la pré- du Initialization inclut l’appel initialisation d’un objet de S au super constructor

staticinitialization(S)

Vrai si cjp correspond à l’exécution de l’initialisation S ::= ClassName de la classe S

handler(TP)

Vrai si cjp correspond à la manipulation de TP spécifie le type d’exception l’exception TP dans un bloc de capture d’exception

within (TP)

Vrai si cjp correspond à l’exécution d’un code TP est le nom de la classe appartenant à TP

withincode(S)

Vrai si cjp correspond à l’exécution d’un code défini S est une méthode ou dans une méthode ou un constructeur spécifié par signature d’un constructeur S

cflow(P)

Vrai si cjp est dans le flot de contrôle du point de jointure défini par P (incluant P lui même)

cflowbelow(P)

Vrai si cjp est dans le flot de contrôle bas du point de jointure P (excluant P lui même)

adviceexecution()

Vrai si le corps en exécution appartient à une consigne

this(TP or Id)

target(TP or Id) args(TP or Id or ‘..’)

la

P est un point de coupure

Vrai si cjp correspond à l’exécution d’un code appartenant à l’objet défini par TP ou Id (L’objet étant l’objet courant référence par this dans JAVA) TP est un nom de classe et Id un identificateur. Vrai si la cible du cjp est un objet spécifié par TP ou ‘..’ remplace n’importe quel Id nombre de paramètres Vrai si les arguments du cjp sont des instances dont le type est spécifié par TP ou Id

if(BoolExp)

Vrai si BoolExp est Vrai

! P

Vrai si P n’est pas satisfait

P1 && P2

Vrai si P1 et P2 sont satisfaits

P1 || P2

Vrai si P1 ou P2 ou les deux sont satisfaits

(P)

Vrai si le point de coupure entre parenthèses est satisfait

BoolExp est booléenne

une

expression

P, P1, et P2 sont des points de coupure

Table 2.1. Signification prédicative des points de coupure primitifs et leur composition Exemple. Le listing 2.1 montre un exemple adapté de [Lad 03], où il est question d’optimiser le calcul de factoriel. On y trouve une classe simple avec une méthode nommée factorial() qui calcule la factoriel d’un entier passé comme paramètre. L’aspect qui optimise le calcul capture les appels de factorial() et met dans un cache les valeurs calculées pour un usage ultérieur. A chaque fois où un calcul de factoriel est lancé, l’aspect capture l’appel et vérifie que le calcul n’a pas été déjà effectué précédemment. Dans le cas contraire, il retourne, à l’appelant de la méthode, la valeur sauvegardée dans le cache. Si la valeur n’est pas dans le cache, le flot d’exécution normal est relancé par proceed(). Notons qu’il est possible de faire la même chose en ajoutant le code de l’optimisation dans la classe ComputeFactorial, cependant une telle implémentation enchevêtre le code de

l’optimisation avec la logique du calcul de la factoriel. En utilisant un aspect, le code d’optimisation est totalement séparé du code fonctionnel, ce qui permet de changer la stratégie d’optimisation sans altérer la classe ComputeFactorial. Programme public class ComputeFactorial { public static long factorial(int n) { if (n==0) return 1; else return n*factorial(n-1);} } public aspect Optimizer { pointcut factOp(int n) : call(* *.factorial(int)) && args(n); pointcut firstCall(int n) : factOp (n) && !cflowbelow(factOp (int));

Commentaires La classe ComputeFactorial utilise la méthode factorial() pour calculer récursivement la factoriel d’un entier n.

Capture les appels de factorial() ainsi que le paramètre Captures uniquement les appels non récursifs de factorial() parmi ceux du point de coupure factOp(). Ignorer les appels récursifs en utilisant !cflowbelow() nous permet de mettre dans le cache uniquement le résultat final. private Cache fCache = new Cache(); fCache est un objet de la classe Cache /* fCache est privé, uniquement cet aspect qui consiste en une liste de paires (int,long). La en a accès */ méthode get(int) retourne une valeur dans le cache et put(int,long) met une valeur dans le cache à une position qui correspond au premier argument. La classe Cache n’est pas donnée ici. before(int n) : firstCall (n) { Cette consigne avant vérifie si l’argument de if (n >50) bigValue(); l’appel est supérieur à 50 puis appelle bigValue() qui génère une exception. La } méthode en question n’est pas donnée ici. long around(int n) : factOp (n) { Cette consigne autour capture l’appel et vérifie si le résultat demandé est dans le cache, long val = fCache.get(n); auquel cas, elle le retourne à l’appelant. Dans le if (val != null) return val; cas contraire, proceed(n) relance return proceed(n); l’exécution normal. Notons que si proceed() } n’est pas exécutée, la consigne suivante (i.e. after returning) n’e l’est pas aussi. after(int n) returning(long result) : Cette consigne capture la valeur retournée après l’exécution de factorial(). Elle firstCall (n) { fCache.put(n, result); } déclare un argument dans returning(), } pour récupérer la valeur retournée, puis utilise le contexte du point de jointure pour mettre à jour le cache.

Listing 2.1. Optimisation de calcul par un aspect

2.2 La composition de filtres Principe et objectifs. La composition de filtres utilise le modèle objet conventionnel et considère l’objet comme une entité qui réalise un certain traitement [Aks 98, Ber 94a]. A l’intérieur d’un système, les entités interagissent pour réaliser un objectif commun. Dans le modèle objet, les interactions ont lieu par envoi et réception de messages. C’est là où la composition de filtres intervient, elle ajoute aux objets une interface contenant des filtres qui interceptent les messages et les manipulent, en modifiant leur portée et les comportements prévus. Alors que la portée fait référence à la délégation du message à d’autres objets (i.e. changement de l’objet cible du message), le changement des comportements prévus est effectué par la substitution des sélecteurs des messages. En contrôlant les messages (i.e. changement des cibles ou des sélecteurs) et moyennant une interface bien construite, la composition de filtres fournit une solution convenable pour résoudre divers problèmes de l’approche orientée objets [Aks 98a]. Aksit et Bergmans citent, en qualité d’auteurs de la

composition de filtres, ces problèmes comme les motivations principales de leur approche. Nous mentionnons, dans ce qui suit, quelques unes de ces motivations : ›

L’héritage dynamique. C’est la possibilité de changer dynamiquement (i.e. durant l’exécution), le lien d’héritage entre classes.

›

Modélisation des comportements qui sont liés à l’état d’un objet. Ainsi, l’objet change de comportement selon son état.

›

Modélisation des comportements qui dépendent de l’historique des objets.

›

Offrir des vues multiples d’un même objet.

›

Synchronisation et coordination.

›

Affichage de traces durant le développement d’un programme.

L’un des avantages de la composition de filtres réside dans l’utilisation d’un mécanisme de filtrage uniforme pour résoudre les problèmes cités précédemment. De ce point de vue, la composition de filtres est simple à comprendre et à utiliser car elle étend le modèle objet classique par quelques nouveaux concepts. La composition de filtres est considérée comme une approche de séparation des préoccupations car les objets de base, dits noyaux (kernel), auxquels on ajoute des interfaces sont laissés inchangés. Ainsi, ils représentent les fonctionnalités alors que les interfaces représentent les préoccupations. La composition de filtres est parfois aussi dite approche orientée aspects, cependant, elle demeure une approche spécifique avec des concepts et un style particulier [Aks 98]. Concepts de base. La composition de filtres ajoute à un objet une couche enveloppante appelée interface qui intercepte les messages entrants et sortants. Un objet doté d’une interface est appelé CF-objet. La figure 2.2 montre le contenu de l’interface ajoutée à un objet.

Filtre d’entrée

Réception de message Réception de message

Filtre d’entrée

Interface de composition de filtres

Méthode

Objet noyau

Méthode

Méthode Méthode

Variable d’instance

Filtre d’entrée

Objet noyau

Méthode Variable d’instance

Message délégué

Méthode Condition

Variable d’instance Variable d’instance

Objet interne Objet Objetinterne

Objet externe

interne

Objet externe

Variable d’instance

Condition

Variable d’instance

Objet externe

Filtre de sortie Envoi de message Filtre de sortie Filtre de sortie

Objet sans filtres

Envoi de message

CF-Objet Flux de contrôle

Référence

Figure 2.2. Objet avant et après ajout d’une interface Une interface se compose des parties suivantes : ›

Objets internes. Ce sont des objets dont les méthodes sont utilisées pour étendre le comportement d’un objet de base. Un message reçu par un CF-objet

peut être délégué à un objet interne à la place du noyau auquel il était initialement adressé. Les objets internes sont encapsulés dans le CF-objet et cessent d’exister si ce dernier est récupéré par le ramasse miettes. ›

Objets externes. Ils sont semblables aux objets internes. Cependant, ils peuvent exister indépendamment des CF-objets. Leurs références sont passées comme paramètres aux constructeurs des CF-objets au moment de leur instanciation. Ces références sont affectées aux variables d’instance correspondantes.

›

Conditions. Ce sont des méthodes spécifiques sans paramètres qui fournissent des informations sur le contexte d’un appel et l’état de l’objet noyau sans les altérer [Ber 94a].

›

Filtres d’entrée. C’est un ensemble de spécifications déclaratives qui manipulent les messages entrants.

›

Filtres de sortie. C’est un ensemble de spécifications déclaratives qui manipulent les messages sortants.

La signature d’un CF-objet est un ensemble de méthodes publiques auxquelles il répond. Cet ensemble inclut les méthodes publiques du noyau ainsi que les méthodes publiques des objets internes et externes de son interface. Dans le cas où deux méthodes sont homonymes, celle du noyau ou de l’objet interne/externe déclaré en premier dans l’interface masque les autres. L’objet cible d’un message est déterminé par le CF-objet lui même quand il reçoit un message et devient une information accessible aux filtres. Les filtres sont déclarés en ensembles ordonnés. Un appel entrant un CF-objet est d’abord réifié (i.e. le sélecteur de la méthode et l’objet cible deviennent accessibles), puis passe chaque filtre de l’ensemble jusqu’à ce qu’il soit écarté ou dispatché. Ecarter un message génère une exception alors que le dispatching consiste en : ›

L’activation de la méthode correspondante dans le noyau ou l’objet interne/externe ou

›

La substitution de l’appel par l’appel d’une autre méthode dans le noyau ou un objet interne/externe et son activation.

Chaque filtre peut accepter ou rejeter un appel selon la sémantique associée à son type. Accepter un appel peut signifier dispatcher ou simplement ignorer le message. Dans ce dernier cas, il passe au filtre suivant. Rejeter un appel peut signifier écarter le message, le mettre en attente dans une queue tant que l’expression associée au filtre résulte en un rejet, ou alors simplement l’ignorer (i.e. le message passe alors au filtre suivant). Il existe cinq types de filtres couramment utilisés : Error, Dispatch, Wait, Meta, et RealTime [Ber 01]. Chaque type est utilisé pour des besoins spécifiques dans l’expression des préoccupations. En général, l’ensemble de filtres contient des filtres de deux ou plusieurs types. Wait est utilisé pour exprimer les préoccupations de synchronisation. Meta permet la réification d’un message de telle sorte que l’accès à ces arguments et aux informations dynamiques (l’émetteur, le recepteur, la valeur retournée, ...) devienne possible. Les filtres de type Realtime permettent l’expression des contraintes temporelles. Error et Dispatch sont utilisés seuls ou en combinaison avec les autres types pour décrire diverses préoccupations. Tous les types de filtres peuvent être utilisés comme filtres d’entrée ou de sortie, excepté que Dispatch est utilisé uniquement dans les filtres d’entrée. Dans plusieurs articles qui traitent de la composition de filtres, les auteurs considèrent que les filtres de sortie opèrent comme les filtres d’entrée et ne nécessitent pas

de traitement spécifique. Comme le paradigme objet tend à adopter le modèle client/serveur, où la responsabilité du serveur est prépondérante, les filtres de sortie sont très peu utilisés. Les préoccupations sont alors associées à l’objet serveur, plutôt qu’éparpillés dans les filtres de sortie de plusieurs objets clients [Ber 94]. Pour améliorer la puissance descriptive de la composition de filtres, chaque filtre est composé de plusieurs éléments appelés éléments de filtre, notés FE, ayant la forme suivante. NomFilter : Type = {ElementFiltre, ElementFiltre, …} Un message entrant passe par chaque élément de filtre qui l’accepte ou le rejette. Encore une fois, la signification du rejet ou de l’acceptation dépend du type de filtre (voir table 2.3). Chaque élément de filtre spécifie une condition C et une liste de paires (matching part, substitution part). Nous appelons cette liste MSPList. Un FE accepte un appel si la condition est vraie et si l’appel s’apparie avec la liste MSPList. La figure 2.3 montre les diagrammes syntaxiques pour différentes formes d’éléments de filtres.

MSPList

Element de filtre MSPList

MSP



Condition de l'appel

MSP

{

}

∼>

,

Matching Part MSP

Matching part

Selecteur Cible

Substitution part

Cible, Sélecteur

Substitution Part (

.

Cible

.

Sélecteur

)

Identificateur

* Figure 2.3. Diagrammes syntaxiques des éléments de filtres Pour simplifier la spécification des filtres, la composition de filtres propose deux opérateurs ⇒ et ∼> appelés respectivement opérateur d’inclusion et opérateur d’exclusion. C ⇒ MSPList signifie que, lorsque la condition C est vraie, tous les messages qui s’apparient avec MSPList sont acceptés. C ∼> MSPList signifie que, lorsque la condition C est vraie, tous les messages, excepté ceux qui s’apparient avec MSPList, sont acceptés. La table 2.2 résume la signification de l’acceptation et du rejet en fonction du type de filtres. La table 2.3, montre quand un élément de filtres accepte ou rejette un message et l’effet que cela pourrait avoir sur le niveau filtre (voir [Koo 95] et [Ber 94] pour plus de détails).

Type de filtre Error

Meta

Wait

Dispatch

Realtime

Actions exécutées Acceptation: Le message accepté passe au filtre suivant dans l’ensemble des filtres Rejet: Une exception est générée Acceptation: Le message accepté est réifié en un objet de la classe Message et envoyé à une méthode d’un méta objet en tant qu'argument. Le méta objet est l’un des objets internes ou externes (le méta objet et sa méthode étant spécifiés dans le FE ayant accepté le message. A l’intérieur de la méthode du méta objet, il est possible d’utiliser trois instructions spécifiques : continue, reply et send [Koo 95]. Continue est utilisé pour réactiver le message réifié qui passe au filtre suivant dans l’ensemble des filtres. Reply est utilisé pour retourner une valeur à l’appelant. Le message réifié n’est alors plus considéré. Avec send, le message réifié est réactivé (comme dans le cas de continue) jusqu’à ce qu’il atteigne l’instruction de retour, de ce fait le méta objet aura accès à la valeur retournée. L’instruction send est suivie par reply ou continue. Aucune substitution ou délégation n’aura lieu. Rejet: Le message rejeté passe au filtre suivant dans l’ensemble des filtres Acceptation: Le message accepté passe au filtre suivant dans l’ensemble des filtres. Aucune substitution ou délégation n’aura lieu. Rejet: le message rejeté est bloqué jusqu’à ce que la condition, correspondant à la matching part qui s’est appariée avec le message, devienne vraie. Le message est alors réévalué par le filtre wait de nouveau Acceptation: Si une nouvelle cible/nouveau sélecteur sont spécifiés dans MSPList, ils sont substitués dans le message accepté, puis ce dernier est envoyé à la nouvelle cible. Les filtres qui restent dans l’ensemble des filtres ne sont plus considérés. Rejet: Le message rejeté passe au filtre suivant dans l’ensemble des filtres Acceptation: Les attributs temporels du message accepté sont changés et le message passe au filtre suivant. Rejet: Le message rejeté passe au filtre suivant dans l’ensemble des filtres Table 2.2. Traitements associés aux filtres selon leur type

Dans la table 2.3, nous considérons qu’un message entrant a T comme objet cible et S comme sélecteur. Selon la valeur de la condition C, et si un appariement avec MSPList a lieu, l’effet sur l’élément de filtre sera d’accepter ou de rejeter le message et ceci aura, à son tour, un effet sur le filtre dans sa totalité. Il acceptera ou rejettera alors l’appel, ou simplement ignorera le message et le laissera passer à l’élément de filtre suivant.

Effet lorsque ⇒ T.S Syntaxe utilisée pour spécifier les Valeur s’apparie est utilisé de C éléments de filtre Sur le Sur le avec MSPList FE filtre C ⇒ { T1.S1, …, Ti.Si} ou Faux Faux/Vrai Rejeter Passer Faux Rejeter Passer C ∼> { T1.S1, …, Ti.Si} Vrai MSPList s’écrit Error {T1.S1, …, Ti.Si} Vrai Vrai Accepter Accepter Aucune substitution ou délégation n’aura lieu Faux Faux/Vrai Rejeter Passer C⇒{T1.S1(MO1.MS1), …, Ti.Si(MOi.MSi)} Vrai Faux Rejeter Passer Meta MOi est le méta objet et MSi l’une Vrai Vrai Accepter Accepter de ses méthodes Rejeter Passer C ⇒ { T1.S1, …, Ti.Si} ou Faux Faux Faux Vrai Rejeter Rejeter ∼ > { T1.S1, …, Ti.Si} C Wait Vrai Faux Rejeter Passer Aucune substitution ou délégation n’aura lieu Vrai Vrai Accepter Accepter Faux Faux/Vrai Rejeter Passer C⇒{T1.S1(NT1.NS1),…, Faux Rejeter Passer Ti.Si(NTi,NSi)} ou Vrai Dispatch C ∼> { T1.S1, …, Ti.Si,} Vrai Accepter Accepter NTi.NSi sont la nouvelle cible et Vrai le nouveau sélecteur Faux Faux/Vrai Rejeter Passer C ⇒ { T1.S1(TC1), …, Faux Rejeter Passer Ti.Si(TCi)} Vrai Realtime TCi est la contrainte de temps Vrai Accepter Accepter Aucune substitution ou délégation Vrai n’aura lieu

Type de filtre

Effet lorsque ∼> est utilisé Sur le Sur le FE filtre Rejeter Passer Accepter Passer Rejeter

Rejeter

∼> n’est pas utilisé dans les filtres Meta Rejeter Rejeter Accepter Rejeter Rejeter Accepter

Rejeter Passer Accepter Passer Passer Accepter

Rejeter

Rejeter

∼> n’est pas utilisé dans les filtres

real time

Table 2.3. Acceptation/Rejet au niveau élément de filtre et son effet sur le niveau filtre Exemple. Comme illustration, considérons l’exemple d’un système de messagerie électronique adapté de [Aks 97]. Le système est composé de quatre classes Sender, Recipient, DeliveryAgent, et la classe principale nommée Email (voir listing 2.2). Sender, Recipient et DeliveryAgent sont des threads qui coordonnent leur comportement en utilisant le modèle du tampon limité. Un thread Sender crée un objet Email, remplit son contenu, indique l’adresse et le met dans le tampon du thread DeliveryAgent. Ce dernier estampille l’objet Email et le délivre dans le tampon de Recipient. Les threads qui disposent d’une référence sur un objet Email, peuvent invoquer toutes ses méthodes. Ainsi, l’agent chargé de délivrer le message électronique, peut lire son contenu, le receveur peut estampiller lui même le message, etc. Ces manipulations ne correspondent pas à des utilisations correctes dans la réalité. Un objet Email devrait fournir des vues multiples en fonction du type du client qui invoque ses méthodes. Par exemple, readContent(…) ne devrait pas être disponible à l’agent qui délivre le message électronique, stamp(…) ne devrait pas être disponible à l’émetteur et au récepteur, etc. Le modèle objet conventionnel ne traite pas directement le problème des vues multiples. Même si un codage de bas niveau est possible, le code sera enchevêtré et les instructions qui implémentent les vues seront éparpillées sur l’ensemble de l’application.

Programme

Commentaires

public class Email { Modifie le contenu d’un message avec un public void champ de type String modifyContent(String text){} public String readContent(){}

Retourne le contenu d’un message

public Boolean deleteContent(){}

Efface le contenu d’un message et retourne un booléen comme indicateur

public void L’adresse transmise dans C devient l’adresse setRecipientAddress(Address C){} du destinataire public Address Retourne l’adresse du destinataire readRecipientAddress(){} public void L’adresse transmise dans C devient l’adresse setSenderAddress(Address C){} de l’expéditeur public Address Retourne l’adresse de l’expéditeur readSenderAddress(){} public Boolean A contient de nouveaux attributs (Marque modifyAttributes(Attributes A){} d’urgence, nombre d’essai de transmission, …) Ajoute la date et l’heure du moment où le message est traité la première fois

public void stamp(Date d, Time t){} }

Listing 2.2. La classe Email Le problème des vues multiples a une solution convenable dans la composition de filtres qui ajoute à la class Email une interface qui contient un ensemble de filtres matérialisant les différentes vues. Les threads peuvent invoquer librement une méthode de la classe Email, cependant, les filtres rejetteront ou accepteront le message selon le type de l’appelant. Le détail de l’interface ajoutée est donné dans le listing 2.3. class Email interface { Internals Externals

// Dans ce cas, il n’y a pas d’objets internes et externes

conditions private Boolean deliveryAgentView(){…} // Retourne vrai si l’appelant est de type DeliveryAgent

private Boolean recipientView() private Boolean senderView ()

{…} {…}

// Retourne vrai si l’appelant est de type Recipient // Retourne vrai si l’appelant est de type

Sender

inputfilters err : Error = {deliveryAgentView() ⇒ {readRecipientAddress, readSenderAddress, stamp}, recipientView() ⇒ { readContent, deleteContent, readSenderAddress, readRecipientAddress }, senderView() ∼> { stamp } } delegate : Dispatch = {true => (inner.)} // Tous les messages qui passent Error sont délégués au noyau appelé inner

} Listing 2.3. Une interface de composition de filtres pour la classe Email

Dans l’interface précédente, le filtre err spécifie que si l’appelant est de type DeliveryAgent,

seuls

les

messages

invoquant

readRecipientAddress,

readSenderAddress et stamp sont autorisés à passer au filtre suivant puis dispatchés au noyau, dans les autres cas, une exception est générée. La liste MSPList du filtre err consiste seulement en une liste de sélecteurs. Si l’appelant est de type Sender, toutes les méthodes sont acceptées à l’exception de stamp. Notons que cet élément de filtre peut être exprimé par l’opérateur d’inclusion. Si on ignore l’héritage de la classe Email, l’élément de filtre s’écrit: senderView() ⇒ {ModifyContent, readContent, deleteContent, setSenderAddress, setRecipientAddress,readRecipientAddress, readSenderAddress, modifyAttributes } Le filtre delegate spécifie que tous les messages, i.e. ceux qui passent le filtre err, vont au noyau sans altération de leur sélecteur.

2.3 La séparation multidimensionnelle Cette approche est le fruit des travaux du laboratoire alphaworks d’IBM et fait suite aux travaux sur l’approche de programmation orientée sujets (Subject Oriented Programming). Principe et objectifs. La séparation multidimensionnelle des préoccupations part de la constatation que les problèmes rencontrés en génie logiciel son dus à l’incapacité de nos approches à gérer de manière adéquate les préoccupations multiples, simultanées et enchevêtrées dont est constitué un système logiciel. Les auteurs de cette approche considèrent que, dans les paradigmes actuels, la séparation est faite selon un nombre réduit de préoccupations [Tar 99]. Ils considèrent aussi que les concepts tels que les fonctions, les classes, les aspects, les objets, les variantes, les points de vue matérialisent, en fait, des préoccupations. Vu que ces concepts sont limités, la description de certaines préoccupations ne pourra pas se faire par des entités de première classe (i.e. la préoccupation n’est pas matérialisée par un concept dédié mais par plusieurs concepts, et donc ne peut être manipulée comme un tout). Par conséquent, la préoccupation sera répartie sur plusieurs éléments de la décomposition dominante. Par exemple, dans Java, le concept de classe impose une description dominante où le système apparaît comme une collection de classes. Briser une préoccupation sur plusieurs éléments de la décomposition dominante conduit à l’enchevêtrement dans ces éléments et l’éparpillement de celle-ci sur les différents éléments. Le principe de l’approche multidimensionnelle considère qu’un système logiciel existe dans un hyperespace multidimensionnel de préoccupations et qu’il devrait être possible de décomposer et recomposer un système donné selon les préoccupations de n’importe laquelle de ces dimensions multiples. De plus, vu que l’ensemble des préoccupations est appelé à changer dans le temps, il devrait être possible de réorganiser le système pour en tenir compte. A l’origine, l’intérêt des auteurs s’est porté sur le niveau programmation puis il s’est très vite réorienté vers les autres niveaux, car l’approche présente une philosophie

radicalement différente qui a un impact sur toutes les phases du cycle de vie d’un système logiciel. Actuellement, il existe une approche générale qui vise à : ›

Fournir un modèle global d’espace de préoccupation qui inclut les préoccupations et leurs relations.

›

Permettre une représentation systématique des espaces des préoccupations indépendamment des formalismes, outils et méthodes.

›

Permettre une représentation consistante et compréhensible des préoccupations.

Pour fixer les idées, nous illustrons l’approche des hyperespaces au niveau programmation par la présentation des concepts du langage HyperJ. Concepts de base. HyperJ est une extension de Java qui implémente le principe des hyperespaces en opérant au niveau des bytecodes Java. Nous décrivons ci-après les concepts clés des hyperespaces [Oss 99]. Les hyperespaces (Hyperspaces) est une approche qui permet une identification explicite de chaque dimension et chaque préoccupation, à n’importe quelle étape du développement, l’encapsulation de ces préoccupations, la gestion des relations entre ces préoccupations et leur intégration. Dans cette approche, l’accent est mis sur quatre concepts principaux: préoccupation (Concern), hyperespace (Hyperspace), hypermodule (Hypermodule) et hypertranche (Hyperslice). Un système logiciel se compose de plusieurs parties qui sont formulées dans un langage donné. Une unité est une construction syntaxique dérivée d’un tel langage. Elle peut être, par exemple, une méthode, une variable d’instance, une procédure, une fonction, une règle, une classe, une interface, etc. Cependant, on distingue les unités simples qui sont considérées comme étant atomiques, et les unités composées qui sont des regroupements d’unités simples. Ainsi, les méthodes, les variables d’instance et les fonctions sont toutes des unités simples tandis que les classes et les paquets de Java sont des unités composées. L’espace des préoccupations renferme toutes les unités d’un système logiciel donné. L’objectif est alors de pouvoir organiser les unités de manière à former et séparer les préoccupations importantes, décrire plusieurs types de relations et indiquer comment le système, et éventuellement ses composants, peuvent être construits et intégrés à partir des unités de ces préoccupations. Le processus de construction d’un système comprend trois étapes : ›

Identification : c’est le processus de sélection de préoccupations ainsi que les unités que renferment ces préoccupations.

›

Encapsulation : consiste à regrouper des unités pour former des préoccupations manipulables en tant qu’entités de première classe. Une classe Java est un exemple d’une préoccupation encapsulée.

›

Intégration : une fois les préoccupations encapsulées, on les intègre pour créer le système final.

L’identification des préoccupations se fait à partir d’une matrice multidimensionnelle dont chaque axe représente une dimension de préoccupation et chaque position sur cet axe une préoccupation donnée. Les dimensions permettent une répartition de toutes les préoccupations en groupes. Chaque élément de la matrice est une unité. Ceci rend explicite

l’identification de toutes les dimensions d’intérêt, les préoccupations qui appartiennent à chaque dimension, et quelles préoccupations sont affectées par quelles unités. Les coordonnées d’une unité indiquent toutes les préoccupations qu’elle affecte. De plus, chaque unité affecte, au plus, une seule préoccupation pour une dimension donnée. Une préoccupation appelée none est créée dans chaque dimension pour les unités qui n’affectent aucune préoccupation de celle-ci. Chaque dimension peut être perçue comme une partition de l’ensemble de toutes les unités (l’espace des préoccupations) : une décomposition particulière du système logiciel. Chaque préoccupation dans une dimension définit un hyperplan qui contient toutes les unités qui affectent cette préoccupation. La figure 2.4 illustre un hyperespace à trois dimensions.

L’unité 5 affecte la préoccupation C2 de la dimension Dim1, C4 de la dimension Dim3 et C3 de la dimension Dim2.

Dim 1

C3 C2 Un hyperplan pour C3 de Dim2

C1 Unité 5 none none C1 C2

none C1

C2

C3

C4

C5

C3 Dim 2

C4 Dim 3

Autres unités

Figure 2.4. Exemple d’un hyperespace La matrice des préoccupations permet d’identifier les préoccupations et situe les unités par rapport à ces préoccupations. Le concept d’hypertranche permet d’encapsuler les unités relatives à chaque préoccupation. L’hypertranche réunit les unités d’une encapsulation et présente la caractéristique de complétude déclarative. Comme les unités se référencent (par exemple une méthode utilise une variable), l’hypertranche déclare tout ce qu’elle utilise. Les déclarations sont abstraites et se résument à nommer les unités utilisées (nom de variable, signature d’une méthode, etc.). La complétude déclarative est très importante puisqu’elle élimine le couplage entre hypertranches, donc, au lieu d’avoir une hypertranche qui fait référence à une autre, chacune d’elles définira ce dont elle a besoin, au moyen de la déclaration abstraite des méthodes et des variables que ses unités invoquent ou utilisent. En conclusion, une hypertranche est une préoccupation et un ensemble de déclarations abstraites.

Les hypertranches construisent des blocs, qui peuvent être intégrés pour former d’autres blocs au moyen des relations d’intégration. Il arrive alors qu’on soit amené à utiliser une hypertranche qui a quelques déclarations abstraites, et qu’on utilise une autre hypertranche qui contient l’implémentation complète de ces déclarations. Ce genre de relations s’appelle la correspondance. L’intégration se fait à l’aide des hypermodules. Un hypermodule est une spécification qui indique un ensemble d’hypertranches et les relations qui servent à les intégrer (les règles de composition). Certains détails sont alors précisés, par exemple, si deux méthodes ont le même nom, leur intégration peut être interprétée par: l’une doit couvrir l’autre, ou l’une complète l’autre et dans quel ordre doivent elles s’exécuter et comment calculer la valeur à retourner. L’hypermodule présente une complétude déclarative, c’est donc une hypertranche composée. Les hypermodules peuvent être utilisés pour modéliser divers produits (artifacts) tels que des spécifications de besoins, une conception ou un code quelconque. Il existe trois stratégies de compositions différentes: mergeByName, overrideByName et noncorrespondingMerge. Dans un hypermodule donné, on utilisera une seule stratégie. ›

mergeByName. Cette stratégie spécifie que les unités, portant le même nom, définies dans différentes hypertranches se correspondent et seront fusionnées. Cela conduit à la création d’une nouvelle unité qui intègre les autres.

›

overrideByName. La dernière unité spécifiée sera l’unité créée (c’est une surcharge).

›

noncorrespondingMerge. Cette stratégie spécifie que les unités, portant le même nom, définies dans différentes hypertranches, ne sont pas reliées par une relation de fusion ou de surcharge. Ceci est utile si les unités ont involontairement le même nom mais n’ont aucune relation de correspondance.

Il existe d’autres formes de relations spécifiques qui se combinent avec les trois stratégies citées précédemment. Nous en donnons un aperçu dans la table 2.4. Relation

Explications

Equate

C’est une relation qui indique qu’un ensemble d’unités se correspondent, même si leurs noms ne sont pas identiques. Cette relation dépend de la stratégie générale de composition. Si cette dernière est mergeByName ou nonCorrespondingMerge, equate fusionnera les unités correspondantes. Par contre, si la stratégie est overrideByName alors equate fera en sorte que l’unité, qui appartient à l’hypertranche qui figurera en dernier dans la déclaration de l’hypermodule, surcharge les autres.

Order

Quand des méthodes sont fusionnées, Hyper/J peut, par défaut, choisir d’exécuter le code des méthodes originales dans n’importe quel ordre. Parfois, l’ordre est significatif et dans ce cas on l’indique par cette relation. Bien que order soit prévu pour affecter l’ordre des méthodes composées, elle peut être appliquée aux hypertranches, aux classes, aux interfaces, aux opérations, et aux actions, mais pas aux champs. Quand une hypertranche ou une classe apparaît dans une relation order, ce sont les unités définies dans ces dernières qui seront affectées par cette relation.

Rename

C’est une directive pour HyperJ, elle indique qu’une unité spécifique dans l’hypertranche composée (mentionnée par le nom de hypermodule) doit avoir un nouveau nom.

Merge

L’utilisation de Merge crée une relation de fusion entre l’ensemble d’unités qui sont indiquées, peu importe la stratégie de composition générale. La différence entre elle et la relation equate est que equate ne fusionne pas les unités opérandes. Merge égalise les unités et les fusionne indépendamment de la stratégie générale de composition. Elle peut être appliquée à n’importe quel type d’unité.

NoMerge

Elle a l’effet opposé à celui de Merge ou Override. Elle empêche les unités qui se correspondent, soit par leur nom ou par la relation equate, de fusionner ou qu’une d’entre elles dépasse les autres, même si la stratégie générale de composition est mergeByName ou overrideByName. noMerge est typiquement employée dans le cas où mergeByName ou overrideByName est employée comme stratégie de composition et lorsqu’il est prévu de ne pas fusionner les unités qui se correspondent. noMerge peut être appliquée à n’importe quel type d’unité ›

Elle indique qu’une unité surcharge une ou plusieurs autres unités. Si nous prenons le cas des méthodes, l’appel de chacune des méthodes définies dans override sera surchargé par l’appel d’une certaine méthode d’entre elles.

›

Elle est employée pour indiquer qu’une unité donnée correspond avec un ensemble d’autres unités indiquées en utilisant un modèle d’appariement (pattern matching). Equate et match ne conduisent pas à intégrer les unités, elles impliquent juste la correspondance, l’intégration est laissée à la stratégie. La relation de correspondance n’a lieu qu’avec les unités de même type, de telle sorte que les classes correspondent aux classes, les variables aux variables, etc.

›

Elle permet de spécifier qu’un ensemble de méthodes S2, doit être précédé par les méthodes de l’ensemble S1 et suivi par celles de S3. En d’autres termes, la méthode résultant de l’intégration sera une fusion d’une méthode de S1, une de S2 et une de S3

›

Quand les méthodes retournant des valeurs sont fusionnées, la méthode composée doit renvoyer une seule valeur. Par défaut, Hyper/J retournera la valeur retournée par la dernière méthode dans la composition. On peut changer cela en choisissant avec cette relation quelle valeur retourner.

Override

Match

Bracket

Summary Function

Table 2.4. Relations spécifiques Exemple. Dans cette section, nous présentons une illustration par un arbre d’expression adapté de [Hyp 05]. Il s’agit d’évaluer des objets expressions arithmétiques simples telles que « 2 » ou « 3 + 4 » sous forme d’arbres syntaxiques abstraits. La figure 2.5 illustre les classes utilisées en UML. L’opération d’affichage (display) renvoie une représentation textuelle d’une expression, alors que l’opération d’évaluation (eval) renvoie la valeur d’une expression.

Exp eval() display() process()

Number

BinaryOperator

eval() display()

setLeftExp(Exp) setRightExp(Exp)

Plus eval() display()

Figure 2.5. Le diagramme de classe de l’arbre d’expression Le listing 2.4 donne le détail et les explications des différentes classes. Programme

Commentaires

public abstract class Exp{ public abstract float eval(); public abstract String display(); public void process(){} }

Exp est la classe supérieure de la hiérarchie. C’est une classe abstraite qui définit seulement les méthodes manipulant une expression. Les sousclasses d’Exp sont prévues pour supporter la définition des méthodes pour évaluer et afficher les expressions.

public class Number extends Exp{ private float number; public Number(float number){} public float eval(){} public String display(){} }

Number est une sous-classe concrète d’Exp qui implémente les méthodes eval() et display(). Number contient l’attribut number, qui mémorise la valeur d’une expression consistant en un nombre. La méthode eval() renvoie la valeur de number, et display() sa représentation textuelle.

public abstract class BinaryOperator extends Exp{ protected Exp leftExp; protected Exp rightExp; public BinaryOperator(Exp leftExp, Exp rightExp){} public void setLeftExp(Exp left){} public void setRightExp(Exp right){} } public class Plus extends BinaryOperator{ public Plus(Exp leftExp, Exp rightExp){} public float eval(){} public String display(){} }

BinaryOperator est une sous-classe abstraite de la classe Exp. Elle contient un constructeur qui prend des expressions gauches et droites d’une opération binaire comme arguments. Dans cette classe, nous avons les méthodes setLeftExp(Exp) les et setRightExp(Exp) pour changer expressions gauches et droites impliquées dans une expression binaire.

Plus est une sous-classe concrète de la classe BinaryOperator. Elle a un constructeur qui prend l’expression gauche et l’expression droite de l’opérateur d’addition comme arguments. Son constructeur appelle le constructeur de la superclasse, BinaryOperator, avec ces arguments. Les méthodes eval() et display() sont implémentées dans cette classe. La méthode eval() renvoie la somme des expressions gauches et droites, et display() renvoie la représentation textuelle de l’expression.

Listing 2.4. Programme Java pour l’arbre d’expression

On souhaite étendre le programme du listing 2.4 pour optimiser le calcul des expressions par utilisation d’un cache. A chaque fois que la méthode eval() est appelée, nous vérifions si le résultat est dans le cache et retournons sa valeur au lieu de refaire les calculs. Pour réaliser cela, selon une approche classique, il faut ajouter à la classe principale Exp une variable ValueInCache, et des méthodes pour accéder à cette variable. Nous devons également modifier toutes les classes qui implémentent les méthodes eval() pour renvoyer le résultat en cache, s’il est valide. De même, si une sous expression est modifiée, on doit propager une information indiquant que la valeur dans le cache n’est plus correcte, aux objets représentant les expressions englobantes. Pour comprendre cela, considérons l’exemple d’expression donné en figure 2.6. Les nombres à l’intérieur des nœuds sont les résultats de l’évaluation des sous arbres au nœud donné. Supposons que nous avons changé le nœud 5 par la valeur 4. Dans ce cas, il sera inutile d’évaluer l’arbre d’expression dans sa totalité une seconde fois car c’est seulement le côté gauche de l’arbre qui n’est plus valide.

22

22

9

13

8

5

1

7

3

9

13

8

6

4

1

7

6

Partie non valide nécessitant un nouveau calcul

3

Figure 2.6. Exemple d’arbre d’expression Nous allons modifier le programme donné dans le listing 2.4 en utilisant l’approche HyperJ. Pour cela, nous effectuons une composition entre la préoccupation de calcul du listing 2.4 avec la préoccupation d’optimisation par le cache. La figure 2.7 illustre les deux parties.

Préoccupation d’optimisation Optimize

Préoccupation de calcul Computing

Exp Exp eval() display() process()

Composition

Number

BinaryOperator

eval() display()

setLeftExp(Exp) setRightExp(Exp)

eval() setCache(float) getCache() setAncestor(Exp) validateCache() invalidateCache() isCacheValid()

BinarOperator setLeftExp(Exp) setRightExp(Exp)

Plus Plus eval() display()

eval()

Figure 2.7. Les deux préoccupations à composer

Le listing 2.5 explique la préoccupation d’optimisation et donne l’hypermodule qui spécifie sa composition avec la préoccupation de calcul. Programme

Commentaires

public abstract class Exp { protected float cache; protected Boolean valid = false; protected Exp ancestor = null; public abstract foat eval(); protected void setAncestor(Exp ancestor){} protected float getCache(){} protected void setCache(float cache){} protected Boolean isCacheValid(){} protected void validateCache(){} protected void invalidateCache(){} }

Dans la classe Exp, on ajoute une méthode pour mettre à jour l’ancêtre d’un nœud. De même, on ajoute des méthodes pour consulter, mettre à jour, valider et invalider la valeur du noeud dans le cache. La méthode invalidateCache() effectue cette opération récursivement pour impliquer tous les ancêtres.

public abstract class BinaryOperator extends Exp{ protected Exp leftExp,rightExp; public BinaryOperator (Exp leftExp,Exp rightExp){} public void setLeftExp(Exp left){} public void setRightExp(Exp right){} }

Lorsqu’on initialise un objet BinaryOperator, on le rend ancêtre de ses deux fils gauche et droit. Ceci peut se faire dans le constructeur de BinaryOperator. Quand on change un des fils de l’objet BinaryOperator par la méthode setLeftExp(Exp) ou la méthode setRightExp(Exp), on rend invalide le cache de l’objet BinaryOperator.

public class Plus extends BinaryOperator

Lorsqu’on évalue le sous arbre à un nœud plus, on vérifie d’abord si le cache de ce nœud est valide. Si c’est le cas, on renvoie la valeur du cache, autrement, on évalue le sous arbre et on met à jour le cache.

{ public Plus(Exp leftExp, Exp rightExp){} public float eval(){} } hypermodule ExpWithCaching hyperslices : dimensionOptimization.caching, dimensionClass.computing; Relationships: MergeByName; order action dimensionClass.computing.Plus.eval Before action

Cet hypermodule spécifie que les deux préoccupations caching et computing situées respectivement dans les dimensions dimensionOptimization et la dimensionClass, seront composées par l’opérateur MergeByName en mettant le corps de l’opération eval() de la préoccupation computing avant celle de la préoccupation caching pour la classe Plus.

dimensionOptimization.caching.Plus.eval; end hypermodule; Listing 2.5. Exemple de code d’optimisation et d’intégration Lorsqu’on emploie la fusion de plusieurs méthodes qui retournent des valeurs, la valeur de retour finale peut être soit la valeur retournée par la dernière méthode fusionnée, ou bien, on implémente une méthode récapitulative ( dite Summary function) pour manipuler la valeur de retour des méthodes fusionnées. Dans notre exemple, nous voulons que la méthode eval() de l’unité Plus dans la préoccupation computing soit exécutée si le cache pour le nœud correspondant dans la préoccupation caching est invalide. Ceci peut être réalisé en accompagnant l’opération de fusion par la clause order dans l’hypermodule ExpWithCaching montré dans le listing 2.5. La structure du nouveau système est illustrée par la figure 2.8.

Exp peval() ndisplay() nprocess() osetCache(float) ogetCache() osetAncestor(Exp) ovalidateCache() oinvalidateCache() i C h V lid()

n Méthodes de la préoccupation computing o Méthodes de la préoccupation caching p Méthodes obtenues par fusion

Number

BinaryOperator

Plus

neval() ndisplay()

psetLeftExp(Exp) psetRightExp(Exp)

peval() ndisplay()

Figure 2.8. Le résultat de la composition des préoccupations

3 LES COMPOSANTS LOGICIELS La dernière décennie a vu naître une orientation vers l’utilisation des composants dans le développement des systèmes et leur structuration architecturale. L’un des moteurs de cette orientation est la confiance qu’a la communauté génie logiciel dans l’impact positif de l’approche sur la productivité lors du développement, la réduction du délai de mise sur le marché d’un logiciel, ainsi que l’amélioration de la qualité, de la fiabilité et de l’évolutivité [Riv 04, Mar 02]. L’approche des composants logiciels s’inspire de l’ingénierie des composants électroniques où les concepteurs construisent de nouveaux systèmes par assemblage de blocs réutilisables tel que microprocesseur, décodeur, multiplexeur, etc. Cette méthode de construction permet un gain de productivité important vu que chaque système est formé à partir de composants qui ont fait leurs preuves et sont produits massivement. Par analogie, au lieu de réaliser un système logiciel ligne par ligne à chaque fois, il serait hautement productif de le réaliser par assemblage de composants logiciels.

3.1 Définition et objectifs Dans une approche orientée composants, une application est vue comme une collection de composants logiciels indépendants, interconnectés à l’aide d’une plate-forme de communication. Chaque composant réalise certaines fonctionnalités et communique avec les autres composants par des points d’entrée et de sortie appelés ports. Chaque port correspond soit à un service que le composant requière ou un service que le composant offre. L’interconnexion est opérée par la plateforme de communication en reliant les différents ports des composants entre eux. L’objectif pratique de l’approche basée composants et d’arriver aux concepts d’étagères électroniques (COTS pour Commercial Off The Shelf) permettant de stocker et de retrouver des composants préconstruits. Dans ce contexte, il serait plus simple de construire une application car il s’agit d’effectuer un assemblage de composants pris des étagères [Riv 04]. Actuellement, les composants logiciels existent sous diverses formes et tailles allant de des petits composants qui peuvent être acquis directement enligne aux composants larges qui contiennent des fonctionnalités étendues et inclus la logique métier d’une entreprise. L’approche orientée composants est importante car elle offre un bon niveau d’abstraction et actuellement on trouve disponibles un nombre important de librairie de composants réutilisables qui facilitent le développement d’applications dans divers domaines. L’objectif général de l’approche orientée composants est de permettre la maîtrise de la complexité

aussi bien au niveau développement qu’au niveau maintenance par la modularisation, l’abstraction et la réutilisation. Il n’est pas aisé de définir avec précision un composant logiciel car les points de vue sont nombreux. Brown présente dans [Brown 1998***] quatre définitions des composants logiciels qui découlent du premier workshop sur le génie logiciel base sur les composants. Nous les présentons ci-après : 1. Un composant est une partie de système non triviale, presque indépendante et remplaçable assurant une fonction claire dans le contexte d’une architecture bien définie. Un composant est conforme à et fournit l’implémentation d’un ensemble d’interfaces. 2. Un composant logiciel exécutable est un package d’un ou de plusieurs programmes gérés comme une unité dont l’édition de liens s’opère dynamiquement et dont l’accès se fait à travers des interfaces bien documentées qui peuvent être identifiées au moment de l’exécution. 3. Un composant logiciel est une unité de composition ayant des interfaces spécifiées par des contrats et seulement une dépendance explicite vis-à-vis de son contexte. Un composant logiciel peut être déployé de façon indépendante et est sujet à la composition par des tiers [Szy 96]. 4. Un composant métier représente une implémentation logicielle autonome d’un concept métier ou d’un processus métier. Il consiste en des artefacts logiciels nécessaires pour implémenter et déployer le concept comme un élément réutilisable d’un système donné. A travers ces définitions, un composant n’est autre qu’un programme (ou une collection de programmes) compilé indépendamment du contexte et est exécutable. Le composant est autonome dans le sens où il fournit une fonctionnalité cohérente et il peut être déployé (installé et exécuté) dans divers environnements. Le composant peut être assemblé avec d’autres composants en utilisant une interface ce qui ne nécessite pas la connaissance de son implémentation.

3.2 Caractéristiques Un composant peut être distingué par quatre caractéristiques [Mar 02] : Type de composant. Il dépend de trois éléments : les interfaces, les modes de communication et la configurabilité. Les interfaces d’un composant sont indépendantes des implémentations, de telle sorte qu’un même type de composants puisse avoir plusieurs implémentations; permettant ainsi de substituer un composant à un autre tout en gardant la même fonctionnalité. On distingue deux types d’interfaces : ›

Interfaces fournies. Elles sont semblables à celles des objets et consistent en une énumération des signatures et données échangées.

›

Interfaces requises. Ce sont les services dont le composant a besoin pour réaliser les services qu’il fournit.

Les composants peuvent avoir divers modes de communication qui s’inscrivent dans les trois catégories suivantes : ›

Mode synchrone. Semblable à l’invocation classique d’une méthode d’objet où l’objet client se bloque en attendant que l’objet serveur exécute la méthode.

›

Mode asynchrone. Correspond à l’utilisation des événements ou des invocations sans retours. Dans ce dernier type, le composant qui demande le service d’un autre n’attend pas de ce dernier qu’il lui renvoie un résultat.

›

Mode diffusion continue. Les composants qui gèrent les flots de données communiquent par ce biais.

Les types de composants se distinguent aussi par les propriétés configurables dont ils disposent. La configuration d’une instance de composant permet de fixer les valeurs initiales des diverses propriétés. Cela peut se faire en invoquant, dès le départ, des services qui fixent un état initial. La phase de configuration consiste aussi à établir des liens entre l’interface requise du composant et les services disponibles dans le système. Implémentation des composants. L’implémentation d’un composant se compose de deux parties, l’une fonctionnelle représentant sa logique métier et l’autre, non fonctionnelle, représentant la gestion des connexions, la sécurité, la persistance, la gestion des transactions, etc. Les besoins non fonctionnels, sont décrits mais générés automatiquement pour chaque composant. La logique métier est à la charge de celui qui développe le composant. Paquetage de composants. Le paquetage de composant désigne l’unité sous laquelle composants sont diffusés et déployés. Il contient une archive, contenant la définition type de composant, une implantation de ce type et une description du contenu paquetage. Un composant diffusé est exécutable après configuration et son intégration nécessite que la connaissance de son type.

les du du ne

Instance de composant. Comme l’instance d’une classe d’objets, l’instance d’un composant possède une référence unique qui l’identifie, un type et une implémentation particulière.

3.3 Composants et cycle de vie d’un logiciel L’approche orientée composants a eu une répercussion sur l’ensemble du cycle de vie au point où actuellement on parle de génie logiciel basé composants (CBSE : Component Based Software Engineering). Dans le CBSE, les composants sont créés/utilisés à tous les niveaux : Analyse, Conception, Programmation et configuration. Le CBSE dans son ensemble vise l’accélération du développement et la réduction du coût à travers l’assemblage de composants préfabriqués. Notons que la conception, l’implémentation et la maintenance des composants en vue de réutilisation se fait à travers un processus complexe qui pose des contraintes aussi bien sur les fonctionnalités et la flexibilité des composants que sur l’organisation qui les produit.

Comparé aux approches classiques de développement, dont les phases sont l’analyse, la conception, la programmation, le test puis l’intégration, le développement en CBSE comporte les phases d’analyse, conception, approvisionnement puis assemblage. L’approvisionnement en composants puis l’assemblage remplacent est simplifient grandement les phases de programmation, test et intégration. Vis-à-vis du composant, les activités de l’ingénierie CBSE peuvent être partagées en deux catégories : Développement pour la réutilisation et développement par réutilisation. Dans la première catégorie, le développement peut être organisé selon les approches classiques on mettant l’accents sur les standards en matière de composants (par exemple, spécification pour chaque composant de l’interface requise et l’interface fournie). Pour la seconde catégorie, on note la prépondérance et l’importance de la recherche et la détermination des composants nécessaires pour assembler une application donnée. Vis-à-vis du processus d’ingénierie, les composants ont différentes formes qui peuvent être classés en cinq catégories [Cheesman 2001]: 1. Spécification de composant. Cette forme représente la spécification d’une unité logicielle qui décrit le comportement des objets composants et définie une unité implémentation Le comportement est défini comme un ensemble d’interfaces. La spécification de composant est par la suite utilisée pour créer une implémentation de composant. 2. Interface de composant. L’interface n’est autre qu’une définition d’un ensemble de comportements qu’un objet composant peut avoir. 3. Implémentation de composant. C’est la réalisation de la spécification de composant qui peut être déployée de manière indépendante. L’indépendance signifie qu’elle peut être installée ou remplacée indépendamment des autres composants mais durant l’exécution il peut y avoir, naturellement, beaucoup de dépendances entre les composants pour atteindre un objectif donné. 4. Composant installé. Cette forme correspond à l’installation d’une copie de l’implémentation du composant. Le déploiement d’une implémentation de composant signifie son enregistrement dans un environnement d’exécution. Cette opération permet, par la suite, à l’environnement d’exécution d’identifier le composant installé à utiliser lors de son instanciation ou l’invocation d’une de ces opérations. 5. Objet composant. C’est l’instance d’un composant installé (un concept de niveau exécution), qui dispose de ses propres données, d’une identité unique et qui exécute les opérations du composant. Un composant installé peut avoir un ou plusieurs objets composants. Dans le cas où il y a un seul objet composant l’identification de l’instance se fait implicitement.

3.4 Exemples Actuellement, il existe de nombreux modèles de composants : CCM, Java Beans, Entreprise Java Beans, Darwin, Olan, etc (voir [Mar 02] pour plus de détails). Dans ce qui suit nous donnons quelques modèles.

3.4.1 JavaBeans et EJB La spécification de JavaBeans [Sun 1997***] définit des composants logiciels nommé Beans, ayant en plus des composants classiques, la caractéristique de pouvoir être construits visuellement en utilisant des outils tel que BDK [Sun1 2001***] ou Bean Builder [Sun2 2003***]. Ainsi les composants JavaBeans components sont naturellement convenables pour développer des applications graphiques. En utilisant JavaBeans, une interface utilisateur graphique (GUI : graphical user interface) peut être construite visuellement par “drag-and-drop” avec un effort minimum de programmation. L’infrastructure des composants JavaBeans fournit au programmeur toutes les techniques qui lui permettent de construire des applications larges par assemblage de composants JavaBeans préconstruits réutilisables. Un composant JavaBeans se caractérise par : ›

Propriétés : Ce sont des attributs publics qui affectent l’apparence du composant tel que sa couleur de fond, la police de caractère qu’il utilise, sa taille graphique, …

›

Méthodes : Ce sont des méthodes Java qui peuvent être invoquées par d’autres

›

Evénements : Un composant bean source peut générer un événement alors qu’un autre composant à l’écoute (listener bean) peut recevoir cet événement et y réagir.

›

Possibilités de personnalisation (Customization) : Les propriétés que le composant

›

Persistance : Possibilité de préservation de l’état d’un composant. C’est grâce à cette caractéristique qu’une application réalisée par assemblage de Beans (et éventuellement changement de leurs caractéristiques) peut être sauvegardée.

›

Introspection : C’est un mécanisme qui permet de retrouver les propriétés d’un composant au moment de l’exécution.

composants JavaBeans ou par l’environnement.

expose peuvent être modifiées au moment de la conception par un éditeur de propriétés.

Pour garantir ces propriétés, l’API de JavaBeans comprend des interfaces et des classes dans le package java.beans et plusieurs autres classes et interfaces du noyau de Java tel que : ›

Modèle d’événement Java: java.util.EventObject, java.awt.event

›

Sérialisation d’objet: java.io.Serializable, java.io.Object

›

Réflexion: java.lang.reflect

En plus des composants JavaBeans, on trouve dans Java aussi les composants EJB (Entreprise JavaBeans) qui sont destinés au coté serveur dans une architecture client/serveur. Le modèle de connexion des ports de JavaBeans permet d’assembler des Beans pour former une applet une application indépendante ou un composant composite.

N’importe quel programme Java peut être transformé en un composant javaBeans. Vu que l’environnement requis pour le fonctionnement de JavaBeans est inclus dans la plateforme Java, il n’est pas nécessaire de distribuer avec un composant une quelconque librairie ou classes. Le composant peut être déployé et exécuté là où on dispose de la plateforme Java. Même si les Beans sont conçus pour être manipulés par un outil graphique de développement, leur représentation graphique n’est pas utilisée au moment de l’exécution. Les valeurs de leurs propriétés peuvent être changées par des éditeurs graphiques mais leurs méthodes et événements sont accessibles au moment du développement de sorte qu’on puisse écrire un code qui manipule le composant au moment de l’exécution. Les composants JavaBeans couramment utilisés ont une partie graphique (i.e. sont visuels et manipulables par un éditeur graphique), c’est le cas des composants des classes AWT et Swing. Cependant, les JavaBeans peuvent être des composants non visuels prenant en charge uniquement des traitements. Le développement des composants Beans doit inclure ce qui suit : ›

Les composants Beans doivent implémenter l’interface Serializable de sorte que leur état puisse être sauvegardé et restaurer ultérieurement (c’est particulièrement le cas des composants ayant une partie graphique).

›

Les méthodes qui sont accessible aux outils de développement et autres Beans doivent être déclarées publiques.

›

Les méthodes publiques des composants doivent être protégées par synchronized pour éviter leurs appels par plus d’un thread à la fois.

›

Les propriétés du composant sont rendues accessibles de l’extérieur par l’utilisation de méthodes get et set.

›

Les événements sont envoyés d’un composant à l’autre et le composant qui est à l’écoute (listner) peut l’être pour une multitude d’événements. Les composants qui génère un événement sont capable de l’envoyer à un nombre quelconque de composants à l’écoute. Pour cela, il gère une liste d’écouteurs dont l’abonnement se fait par des méthodes dont le nom est normalisé (commence par add ou remove).

Les Entreprise JavaBeans ou EJB sont des composants serveurs donc non visuel qui respectent les spécifications d’un modèle édité par Sun. Le respect de ces spécifications permet d’utiliser les EJB de façon indépendante du serveur. Le but des EJB est de faciliter la création d'applications distribuées pour les entreprises. Une des principales caractéristiques des EJB est de permettre aux développeurs de se concentrer sur les traitements orientés métiers car les EJB et l’environnement dans lequel ils s’exécutent prennent en charge un certains nombres de traitements tel que la gestion des transactions, la persistance des données, la sécurité, ... Physiquement, un EJB est un ensemble d'au moins deux classes regroupées dans un module contenant un descripteur particulier. Les EJB doivent obligatoirement s'exécuter dans un environnement serveur dédié (JBoss, Websphere, …). Les EJB sont parfaitement adaptés pour être intégrés dans une architecture trois tiers ou plus. Dans une telle architecture, chaque tiers assure une fonctionne particulière :

1- Le client assure la saisie et l’affichage des données (interface avec l’utilisateur) 2- Au niveau du serveur, les objets métiers contiennent les traitements (des EJB) 3- Une base de données assure la persistance des informations Les EJB s’exécutent dans un environnement particulier : le serveur d’EJB. Celui ci fournit un ensemble de fonctionnalités utilisées par des conteneurs d’EJB (qui constituent le serveur d’EJB). Un serveur peut contenir plusieurs conteneurs. En réalité, c’est dans un conteneur que s’exécute un EJB. Il existe de nombreux serveurs d'EJB commerciaux : BEA Weblogic, IBM Webpshere, Sun IPlanet, Macromedia JRun, Borland AppServer, etc ... Il existe aussi des serveurs d'EJB open source dont le plus avancé est JBoss. Les entités externes au serveur qui appellent un EJB ne communiquent pas directement avec celui ci. Un objet héritant de EJBObject assure le dialogue entre ces entités et les EJB. Il existe trois types d’EJB : les Beans de session (session Beans), les Beans entité (entity Beans) et les Beans orienté message (message driven bean). Ils ont comme points communs la nécessité d’être déployés dans des conteneur d’EJB lesquels s’exécutent dans un serveur d’EJB.

Un EJB session est un EJB de service dont la durée de vie correspond à un échange avec un client. Ils contiennent les règles métiers de l'application. Les session Beans peuvent être de deux types : sans état (stateless) ou avec état (statefull). Les Beans de session sans état peuvent être utilisés pour traiter les requêtes de plusieurs clients. Les Beans de session avec état ne sont accessibles que lors d’un ou plusieurs échanges avec le client. Le bean peut conserver des données entre les échanges. Les session bean statefull sont capables de conserver l’état du bean dans ses variables d’instance durant toute la conversation avec un client. Mais ces données ne sont pas persistantes : à la fin de l’échange avec le client, l’instance de l’EJB est détruite et les données sont perdues. Les Beans entité permettent de représenter des données enregistrées dans une base de données. Ils implémentent l'interface EntityBean. Les Beans entité assurent la persistance des données en représentant tout au partie d’une table ou d’une vue. Plusieurs clients peuvent accéder simultanément à un même EJB entity. La gestion des transactions et des accès concurrents sont assurés par le conteneur. Il existe deux types d’entity bean : 1- Composant dont la persistance est gérée par le conteneur (CMP : Container-managed persistence) Pour un bean CMP, c’est le conteneur d’EJB qui assure la persistance des données grâce aux paramètres fournie dans le descripteur de déploiement du bean. 2- Composant assurant la persistance (BMP : bean-managed persistence). Un bean BMP assure lui même la persistance des données grâce à du code inclus dans le bean.

Le cycle de développement d’un EJB comprend : 1- La création des classes du bean 2- La packaging du bean sous forme de fichier archive jar 3- Le déploiement du bean dans le serveur d’EJB 4- Le test du bean La création d’un bean nécessite la création d’au minimum trois entités pour respecter les spécifications de Sun : la classe du bean, l’interface remote et l’interface home. L’interface remote permet de définir l’ensemble des services fournis par le bean. Cette interface étend l’interface EJBObject. L’interface home permet de définir l’ensemble des services qui vont gérer le cycle de vie du bean. Cette interface étend l’interface EJBHome. La classe du bean contient les traitements du bean. Cette classe implémente les méthodes déclarées dans les interfaces home et remote. L'accès aux fonctionnalités du bean se fait obligatoirement par les méthodes définies dans les interfaces home et remote.

3.4.2 Modèle de composant CORBA Pour faciliter et augmenter la qualité du processus de production d’applications distribuées à base d’entités logicielles hétérogènes et multifournisseurs, l’OMG a défini le CORBA Component Model (CMM) [OMG 99***]. CMM un modèle de composants logiciels serveurs répartis basé sur une technologie d’environnements d’exécution génériques similaire à ceux des EJBs. Le modèle abstrait des composants CORBA vise essentiellement deux choses. Premièrement, un type de composant décrit l’extérieur d’une boîte noire dont on ne sait rien de l’implantation. Deuxièmement, seule implémentation fonctionnelle devrait être à programmer et le reste décrit. Le premier point met en avant la volonté de rendre explicite toute interaction avec ou à partir d’un type de composant. Dans ce sens, un type de composant ne se limite pas à définir les services qu’il fournit, mais aussi ceux qu’il utilise. Le second point tend à faciliter la production et à accroître la réutilisabilité des implémentations de composants. La description sert de base à la génération automatique de la prise en charge des aspects non fonctionnels. Un composant CORBA est une entité potentiellement multiports. Pour permettre la définition des types de composants, le langage OMG IDL a été étendu pour prendre en compte les nouveaux concepts comme les ports. Ce langage de description permet d’exprimer les services fournis ainsi que les services requis par un composant. La connectique entre instances de composants est exprimée sémantiquement (par des motsclés réservés) et non syntaxiquement (par des règles de nommage des méthodes comme les JavaBeans). Pour permettre une bonne intégration des parties fonctionnelles et non fonctionnelles de l’implantation d’un composant, le CCM fournit un canevas, le Component Implémentation

Framework (CIF), qui spécifie comment les deux parties de implémentation doivent coopérer. De plus, ce canevas fixe comment est générée la partie non fonctionnelle. Pour permettre cette génération, le CIF s’appuie sur un langage déclaratif, le Component Implémentation Définition Language (CIDL), qui permet de décrire les aspects non fonctionnels d’un composant (persistance, sécurité, …). Comme dans le cadre des EJBs, les composants CORBA s’exécutent dans un conteneur qui leur offre d’une part un environnement d’exécution (espace mémoire, flot d’exécution), et d’autre part les services systèmes nécessaires au composant. Les échanges entre instances de composant et conteneur sont définis par des APIs standardisées. Une fois le type de composant définit en IDL et implanté selon le CIF, un paquetage regroupe ces divers éléments ainsi que leur description pour permettre la diffusion du composant. Il est alors possible de composer des paquetages pour produire des applications ou bien de déployer directement ces composants. Dans le but d’automatiser au maximum la phase de déploiement des applications à base de composants CORBA, la spécification décrit le langage OSD (Open Software Description) pour fournir les informations nécessaires au déploiement. Ce langage spécifié par le W3C et étendu par l’OMG est défini comme un vocabulaire XML [W3C 98] (eXtensible Markup Language). Il permet de décrire d’une part les besoins des implémentations de composants (persistance, sécurité, transaction) et d’autre part les assemblages de composants. L’architecture d’une application est ainsi décrite de manière abstraite et pourra être projetée sur un ensemble de ressources physiques lors du déploiement. Un des apports essentiels du CCM est l’ensemble des moyens fournis pour décrire les types, les implémentations et les assemblages de composants. Cette proposition fait un grand pas vers la non-programmation des applications : décrire au mieux pour programmer au minimum. Cette puissance d’expression prend tout son sens sur les aspects non fonctionnels des composants qui peuvent, pour une même implémentation, varier en fonction du contexte d’exécution. Le second atout majeur des composants CORBA est le fait d’expliciter les dépendances des composants, en termes de composants et de ressources système, pour permettre une composition dynamique des instances. Ensuite, le modèle de déploiement est une première réponse encourageante, vers une automatisation du processus de déploiement des applications distribuées. Enfin, le CCM est la seule proposition qui veut permettre la construction d’applications à l’aide de composants et d’outils multifournisseurs. Dans la même idée, le CCM représente une des solutions les plus complètes à ce jour. L’accent est mis sur la description des applications plutôt que sur leur programmation, aspects non fonctionnels, assemblage de composants, contraintes techniques, etc. De plus, une première solution acceptable est proposée pour le processus de déploiement. Enfin, le CCM est clairement orienté multifournisseurs.

3.4.3 COM et DCOM COM (Common Object Model) [ROG 97***] et DCOM (Distributed COM) [EDD 98***] représentent la proposition de Microsoft en termes de composants logiciels. La proposition COM résulte d’une évolution progressive de l’environnement Windows vers les composants. De par son processus de conception, COM ne s’appuie pas sur un modèle réel. Dans le cadre de COM, un composant est essentiellement une entité binaire pour laquelle sont définis une interface et son mode d’interaction. Un composant peut offrir plusieurs interfaces, mais il dispose au moins de l’interface de base offrant la découverte et la navigation parmi les interfaces fournies. Une interface optionnelle peut être définie sur un composant pour permettre une utilisation dynamique et générique (connexion/déconnexion et invocation d’opérations). COM n’impose pas de contrainte sur l’implantation de cette interface qui n’est pas forcément objet. Cette dernière se résume à la fourniture d’une bibliothèque qui sera instanciée dans l’environnement Windows. DCOM est une extension de COM pour prendre en compte l’aspect distribué, principalement multiprocessus, d’une application. La distribution est masquée par l’utilisation de stubs et de proxy prenant en charge la partie communication. COM+ est quant à lui une extension incluant un modèle d’objets légers (lightweight object model) à COM. Il représente aussi l’intégration du moteur transactionnel de Microsoft MTS (Microsoft Transaction Server), du service de messagerie MSQN (Microsoft Message Queing), et d’autres services comme la persistance. COM+ est un modèle uniquement utilisable dans un contexte intraprocessus. Les outils système de COM pour la production d’applications se résument essentiellement à un environnement graphique de développement comme MS Visual Studio. Cet environnement fournit un ensemble de moyens pour produire une application : production de bibliothèque, d’exécutables, assistants pour la génération automatique de code, etc. La diffusion d’implantations de composants se fait principalement au travers de bibliothèques (DLL Windows). Ces bibliothèques sont à déployer manuellement sur les machines cibles, selon une solution ad hoc. L’assemblage de composants est essentiellement réalisé à partir d’un environnement tel MS Visual Studio, la documentation des bibliothèques n’étant pas disponible dans ces dernières. COM est une réponse de Microsoft à l’évolution du marché des applications. Microsoft fut un des premiers avec les modules Visual Basic à proposer une solution industrielle orientée composant. COM est donc une solution Microsoft pour l’environnement Windows.

3.4.4 Fractal Le modèle de composants Fractal a été proposé par le consortium ObjectWeb comme un modèle qui permet l’implémentation, le déploiement et la reconfiguration dynamique des systèmes logiciels complexes [Bru 04]. Ses principales caractéristiques sont : ›

Les composants Fractal sont des composites pouvant contenir récursivement d’autres composants Fractal. Cette possibilité permet d’avoir une représentation uniforme quelque soit les niveaux d’abstraction.

›

Les composants Fractal peuvent être partagés entre plusieurs autres composants.

›

Il est possible d’opérer une introspection d’un composant permettant de connaître dynamiquement ses propriétés.

›

Il est possible d’opérer une configuration et une reconfiguration dynamique permettant le déploiement et le changement à tout moment (on parle d’intercession).

›

Il est possible de modéliser une large gamme d’applications, allant des systèmes embarqués aux systèmes d’informations.

Pour tenir compte de la dégradation des performances que ce modèle peut introduire, les auteurs proposent de donner aux composants une forme libre qui permet au développeur de les doter au choix de divers aspects. Ainsi, le modèle Fractal ne définit pas des spécifications auxquelles chaque composant doit se conformé, mais plutôt propose un système de relations extensibles qu’un composant Fractal implémente en fonction de ce qu’il doit offrir comme services aux autres composants. Au niveau le plus bas, un composant Fractal est une entité exécutable, comme un objet, qui n’offre aucune possibilité permettant aux autres composants de le contrôler (i.e. introspection ou intercession). Tout ce que ces derniers peuvent faire se limite à l’invocation des méthodes du composant. A un niveau plus haut, le composant offre des possibilités d’introspection via une interface standard qui permet aux autres composants de découvrir tout ce qu’il offre comme services. Ces services sont décrits sous forme de plusieurs interfaces semblables à celles de Java. A un niveau supérieur, un composant Fractal fournit des interfaces de contrôle qui permettent l’introspection et la modification de son contenu. Dans le modèle Fractal, le composant peut contenir plusieurs sous composants reliés par des liaisons. Il est possible alors de doter, ou non, un composant d’une interface qui permet de contrôler ses sous composants et leurs liaisons. Vu la nature optionnelle des différentes caractéristiques (introspection et intercession), il est possible de réaliser en Fractal divers types d’applications, allant des applications optimisées sans possibilités de reconfiguration aux applications moins optimisées mais hautement dynamiques et reconfigurables. Vu de l’intérieur, un composant Fractal est formé de deux parties : Un contrôleur (ou membrane) et un contenu. Le contenu n’est autre qu’un ou plusieurs autres composants qui sont sous le contrôle du composant composite qui les englobe. Les composants sont connectés ensemble via des liaisons (Figure 3.9).

n

t

e

r

f

I n t e r f a c e s f o n c t i o n n e l l e s s e r v e u r s

Contrôle des attributs

a

c

e

Liaisons

s

d

e

c

Cycle de vie

o

n

t

r

ô

l

e

Contrôle du contenu

Contrôleur Contenu

Sous composant

I n t e r f a c e s f o n c t i o n n e l l e s c l i e n t e s

I

Figure 3.9. Forme d’un composant Fractal Une liaison est un chemin de communication entre les interfaces des composants. Fractal définit deux types de liaisons : celles primitives qui représentent de simples références entre deux interfaces de composants (l’une cliente et l’autre serveur) se trouvant dans un même espace d’adressage, et celles composites qui relient des composants se trouvant dans des espaces d’adressage différents (éventuellement sur différentes machines). Le contrôleur d’un composant peut avoir des interfaces externes et internes. Les interfaces externes sont accessibles de l’extérieur et les interfaces internes sont accessibles uniquement des sous composants. On distingue deux types d’interfaces : les interfaces fonctionnelles qui correspondent aux fonctionnalités métier (fournies ou requises) et les interfaces de contrôle qui sont optionnelles et permettent de manipuler, par exemple, les attributs du composants (e.g. couleur, taille des champs configurables, ), les liaisons ou l’arrêt/relancement de l’exécution du composant (cette dernière possibilité est assure par l’interface nommée cycle de vie), ... En général, le contrôleur d’un composant implémente la sémantique de composition des sous composants. Il fournit une représentation explicite des sous composants et leur liaisons, intercepte les invocations entrantes ou sortantes dont les sous composants sont cibles ou sources, superpose des traitements de contrôle aux comportements des sous composants en les suspendant, les relançant, etc.

4 LES MODELES DE CONCEPTION Les modèles de conception orientés objets aident un concepteur ou un programmeur à construire son application. Ils représentent des formes d’expertises validées sur lesquelles un consensus a été atteint. Les modèles de conception sont pratiques dans la conception des logiciels et ont un apport appréciable. L’origine des modèles de conception remonte aux travaux d’Alexander Christopher sur la conception urbaine et l’architecture des constructions, publiés dans son livre « The timeless way of building » [Ale 79]. Ses idées furent ensuite empruntées par la communauté génie logiciel et donnèrent naissance aux modèles de conception orientés objets.

4.1 Définition et objectifs Un modèle de conception est une description textuelle d’un problème de conception et une solution générale pour ce problème dans un contexte particulier [Ale 79]. Selon Alexander: “Each pattern is a three-part rule, which expresses a relation between a certain context, a problem, and a solution”. Qu’on peut traduire par: Chaque modèle est une règle en trois parties exprimant une relation entre un contexte donné, un problème et une solution. Toujours selon Alexander, un modèle de conception est en même temps une chose (‘a thing’) qui survient dans la réalité, et une règle qui nous dicte comment et quand créer cette chose. C’est à la fois une chose et un processus qui la génère. Coplien compare les modèles de conception aux patrons de coupe pour la confection des vêtements [Cop 96]. Le patron guide la coupe et indique à une personne comment réaliser le travail, même si cette personne n’a pas une idée précise de ce qu’elle réalise. Les modèles de conception ne sont pas des méthodes complètes de conception mais ils véhiculent des pratiques importantes des méthodes existantes. Ils constituent, de ce fait, un moyen de communication et d’échange entre les intervenants du processus de développement. L’approche des modèles de conception essaye d’extraire les informations empiriques importantes de conception. Ce qui les différencie de la simple réutilisation de programmes d’un catalogue, est le fait qu’ils capturent des abstractions plus générales et des structures qui ne sont pas directement visibles dans les documents de conception ou le code d’une application. Selon Coplien, l’objectif des modèles de conception peut être résumé en deux point: ›

Capture des pratiques importantes. Les modèles de conception capturent des pratiques plutôt intangibles, récurrentes et importantes d’un domaine donné. Un

modèle de conception est abstrait par nature car il capture une pratique avec une certaine généralité, en ignorant les détails. Un compromis est cependant recherché: avoir un modèle avec suffisamment de détails pour qu’un concepteur sache quoi faire et suffisamment de généralité pour qu’il puisse l’utiliser dans divers contextes. ›

Les modèles de conception capturent des structures cachées. Les modèles de conception entrecoupent les entités du découpage prédominant utilisé dans un contexte donné. Par exemple, des modèles décrits par Alexander traitent de la transition d’entrée (i.e. le passage d’une rue à une construction donnée, de l’extérieur à l’intérieur) et caractérisent cette transition. Les modèles traitent souvent des relations entre les entités et rarement des entités elles même prises séparément. Les modèles de conception orientés objets s’intéressent aux problèmes des systèmes qui sont non perceptibles sous l’angle de vue de l’objet lui même. Les modèles de conception complètent les méthodes orientées objets car ils capturent des constructions qui entrecoupent les objets. Ces constructions sont souvent cachées car la décomposition dominante (l’objet) ne les met pas en évidence. Cet aspect des modèles de conception est résumé par Alexander comme suit:

›

“Design is often thought of as a process of synthesis, a process of putting together things, a process of combination.”

Que nous traduisons par: la conception est souvent pensée comme un processus de synthèse, un processus d’assemblage de choses, un processus de combinaison.

4.2 Caractéristiques et qualités La description textuelle d’un modèle de conception se compose de plusieurs éléments distincts. Il existe actuellement plusieurs formes de descriptions [Cop 96, Gam 95], cependant, elles partages toutes les éléments essentiels que nous décrivons ci-après [App 00]. Le nom. Le modèle doit avoir un nom significatif simple ou composé qui le reflète (i.e. une phrase courte). Ce nom sera utilisé couramment dans le vocabulaire des concepteurs. Parfois, certains modèles ont plusieurs noms différents et il est commun d’indiquer ce fait dans une section spéciale appelée Alias ou Nommé aussi. Dans certain cas, le nom du modèle est accompagné par une indication sur la classe à laquelle il appartient. Résumé. C’est une présentation courte qui est destinée à aider le concepteur dans la recherche d’un modèle qui résout son problème. Problème. C’est une description qui décrit les objectifs que le modèle vise à atteindre dans un contexte donné. Contexte. Ce sont les préconditions sous lesquelles le problème et sa solution semblent apparaître de manière récurrente, et pour lesquelles la solution est souhaitable. En d’autres termes, cet élément est à consulter pour avoir une idée sur l’applicabilité d’un modèle de conception donné. On peut aussi considérer que c’est une configuration initiale d’un système avant qu’on ne lui applique le modèle de conception.

Forces. Le modèle de conception est souvent un compromis entre plusieurs forces et contraintes. Cet élément décrit comment ces forces et contraintes interagissent et entrent en conflit avec les objectifs qu’on souhaite atteindre. Un scénario concret relatant les motivations du modèle est souvent utilisé. Une bonne description doit cerner toutes les forces et contraintes qui s’exercent sur un modèle. Solution. Cet élément consiste en des relations statiques et des règles dynamiques qui décrivent comment atteindre le résultat souhaité. La description de la solution peut se faire à base de schémas, diagrammes, et phrases identifiant la structure du modèle, les intervenants et leur collaboration. La description inclut la structure statique qui nous renseigne sur la forme et l’organisation du modèle, mais aussi le comportement dynamique. Dans de nombreux cas, la description relate des pièges à éviter et des directives qui nous guident vers la solution. La solution peut exister en plusieurs variantes, il faut alors décrire les différentes possibilités qui s’offrent au concepteur. Exemples. Cet élément consiste en la description d’une ou plusieurs applications montrant un contexte initial, comment le modèle de conception et appliqué, comment il transforme ce contexte et sous quelle forme il le façonne. Les exemples aident le concepteur à maîtriser le modèle et son application, de ce fait, des exemples simples, faciles à comprendre et souvent rencontrés, sont préférables. Le contexte obtenu. Cet élément décrit les post conditions et les effets de bord du modèle. La configuration du système après application du modèle de conception est donnée avec les conséquences aussi bien bonnes que mauvaises, le compromis entre forces et contraintes est décrit tout en mettant en évidence d’autres problèmes et modèles de conception applicables que le nouveau contexte laisse émerger. Raisonnement. C’est une explication justificative des étapes et règles du modèle montrant comment le modèle parvient à un compromis entre les forces impliquées et comment cela s’aligne avec les objectifs souhaités. Cela permet d’indiquer que le modèle fonctionne, pourquoi il fonctionne et pourquoi il est convenable. Les modèles de conception en relation. Cet élément indique les relations que le modèle a avec d’autres modèles. Une relation peut s’exprimer en termes de forces et contraintes communes, en termes de ressemblance et compatibilité au niveau contexte initial ou contexte résultat ou bien en termes de succession d’application (le contexte résultant de l’un est un contexte initial compatible pour l’application d’un autre modèle). Des modèles de conception alternatifs qui s’accommodent d’autres forces et contraintes, s’ils existent, doivent être indiqués. Les modèles qui doivent s’appliquer en même temps que celui là, et dont ils dépendent, doivent aussi être indiqués. Les utilisations connues. Décrit les applications connues de ce modèle de conception. Cela permet de démontrer que le modèle est une solution au problème considéré. En plus des éléments précédents, les modèles peuvent être caractérisés par certains critères de qualité [Dou 04]: Encapsulation. C’est le fait que le problème et la solution soient bien délimités. Au niveau implémentation, cela peut se traduire par une meilleure localisation du code.

Abstraction. C’est le fait que le modèle représente une expertise abstraite qui peut être utilisée à différents niveaux hiérarchiques dans un même domaine. Ouverture. C’est la possibilité d’extension ou paramétrisation du modèle par d’autres modèles afin de résoudre un problème plus large que celui résolu par chacun d’eux. Variabilité. Exprime la possibilité d'implémenter un modèle de diverses façons. Composabilité. L’application des modèles peut être linéaire dans le sens où le contexte résultat de l’un devient le contexte initial d’un autre. Cependant, la composition peut être quelconque et intervenir sur différents niveaux hiérarchiques d’abstraction. La composabilité caractérise l’aptitude d’un modèle à être composé avec les autres. Equilibre. Il s’agit du compromis entre les forces et les contraintes que le modèle permet de préserver.

4.3 Classification Dans le domaine de la conception orientée objets, l’appellation « modèle de conception » est maintenue car elle a été utilisée par Gamma et al. dans le livre [Gam 95] qui est peut être la référence la plus citée dans le domaine. Cependant, les modèles peuvent concerner différents niveaux. Le plus souvent, on sépare les modèles de conception en trois classes correspondant aux trois niveaux d’abstraction: architectural, conceptuel et programmation. On parle alors de modèles architecturaux, de modèles conceptuels et de modèles de codage. Modèles architecturaux. Ils s’adressent à l’organisation structurelle fondamentale d’un système en fournissant un ensemble de sous systèmes prédéfinis, spécifiant leur relation au moyen de règles et de directives. Les modèles architecturaux sont considérés comme des stratégies qui régissent les composants larges et les propriétés et mécanismes globaux d’un système. Modèles conceptuels. Ils fournissent des schémas de raffinement des sous systèmes ou composants d’un système logiciel et des relations qui les lient. Ils décrivent aussi les structures récurrentes de composants et leur communication pour résoudre un problème de conception général dans un contexte donné. Les modèles conceptuels n’ont pas d’influence sur le système dans sa globalité mais définissent plutôt des microarchitectures de soussystèmes. Modèles de codage. Appelés aussi idiomes, ils sont spécifiques à un langage de programmation donné et décrivent comment implémenter un aspect particulier des composants et des relations qui les lient, en fonction des caractéristiques du langage en question. Il existe actuellement de nombreux modèles de conception et leur nombre ne cesse de grandir. Les plus utilisés sont ceux décrits dans [Gam 95], on cite : Factory, Prototype, Singleton, Adapter, Bridge, Composite, decorator, Façade, Proxy, Interpreter, Iterator, Mediator, Memento, Observer, Strategy, Template, Visitor.

4.4 Exemples de modèles de conception Pour illustrer l’approche des modèles de conception, nous présentons un exemple tiré du livre [Gam 95]. Nous le décrivons moyennant les éléments présentés en 4.2. Nom. Modèle Composite. Résumé. Créer récursivement des objets composites à partir d’objets composants (pouvant être composites à leur tour) et pouvoir les manipuler dans les objets clients de la même façon et sans tenir compte de leur aspect composé. Problème. Le modèle composite compose des objets en des structures arborescentes pour représenter des hiérarchies composant/composé. Il permet au client de traiter d'une même et unique façon les objets individuels et leurs combinaisons. Forces. Les applications graphiques telles que des éditeurs de dessins permettent à l’utilisateur de construire des diagrammes complexes à partir de digrammes simples. L’opération de composition peut se répéter récursivement selon le besoin. Une implémentation simple serait de définir des classes qui correspondent aux primitives graphiques (e.g. ligne, texte) et de définir des classes conteneurs dont les objets peuvent contenir des primitives graphiques. Cependant, le code des objets clients qui manipule ces classes doit traiter chacune différemment. Le fait d’avoir à faire cette distinction rend l’application complexe. Le modèle composite décrit l’utilisation de la composition récursive qui dispense les clients d’avoir à faire cette distinction. L’élément fondamental du modèle composite est une classe abstraite qui représente à la fois les primitives et leurs conteneurs. Dans le cas d’une application graphique, la classe abstraite peut être nommée Graphic et offrir des opérations telles que drawing ainsi que des opérations d’accès aux composants. Les sous classes de la classe abstraite, telle que ligne, rectangle, texte, implémentent l’opération drawing mais pas les opérations d’accès aux composants parce qu’elles n’en ont pas. On ajoute une classe qui sera la classe des objets composites nommée Schema. Cette dernière implémente l’opération drawing de façon à faire appel à l’opération drawing des objets composants et implémente les opérations qui manipulent les composants. Comme l’interface de Schema est conforme à l’interface de Graphic, les objets de Schema peuvent composer récursivement d’autres objets Schema. La figure 4.1, donne une représentation UML d’une partie d’un éditeur graphique.

Graphic drawing() addComponent(Graphic c) deleteComponent(Graphic c) accessComponent(int i) listComponents

Line

Rectangle

Text

Schema

drawing()

drawing()

drawing()

drawing() addComponent(Graphic c) deleteComponent(Graphic c) accessComponent(int i)

Pour chaque composant c de listComponents invoquer c.drawin() Ajouter c à listComponents

Figure 4.1. Classes d’un éditeur graphique Solution. La structure utilisée dans ce modèle est celle donnée dans la figure 4.2. Le rôle de chaque classe est donné juste après celle-ci.

Client

Component anOperation() addComponent(Component c) deleteComponent(Component c) accessComponent(int i) listComponents

Leaf

Composite

anOperation()

anOperation() addComponent(Component c) deleteComponent(Component c) accessComponent(int i)

Pour chaque composant c de listComponents invoquer c.anOperation() Ajouter c à listComponents

Figure 4.2. Classes du modèle composite La classe Component déclare l’interface des objets entrant dans la composition, implémente le comportement par défaut qui convient pour toutes les sous classes, déclare une interface pour l'accès aux composants fils et, éventuellement, définit une interface pour accéder au père d’un composant et l’implémente. La classe Leaf (i.e. feuille) représente les classes sans composants fils et implémente le comportement des objets primitifs. La classe Composite définit le comportement des composants ayant des composants fils. Elle utilise une structure pour stocker les composants fils et implémente les opérations, liées aux composants fils, définies dans la classe abstraite Component. La classe Client est la classe qui manipule les objets composites à l’aide de l’interface disponible dans Component.

Exemple. Le listing 4.1 donne un exemple de code Java où il s’agit de matériel informatique pouvant être simple ou composite. Programme

Commentaires

public abstract class Equipment { protected String name; protected String name(){return name} Abstract float power(); Abstract float price(); Abstract Equipment getEquip(int i); Abstract void addEquip(Equipment e); Abstract Equipment deleteEquip(int i); }

La classe Equipment est une classe abstraite qui déclare un champ représentant le nom de l’objet et une méthode qui retourne ce nom. Elle déclare aussi cinq méthodes abstraites qui calculent la puissance et le prix, retournent un équipement qui est à la position i de la liste des composants, ajoutent un équipement dans la liste, ...

public class CompositeEquip extends Equipment{ public float power(){} public float price(){} public Equipment getEquip(int i) {} public void addEquip(Equipment e) {} public Equipment deleteEquip(int i) {} }

Un équipement peut être composé de plusieurs équipements. Cette classe implémente les deux premières méthodes abstraites de Equipment, par un parcours de la liste des composants.

public class Processor extends Equipment{ public float power(){return 200} public float price(){return 100} public Equipment getEquip(int i) {return this} public void addEquip(Equipment e) {} public Equipment deleteEquip(int i) {return null} }

Processor est une classe d’équipement simple qui implémente les méthodes abstraites

public void main (...) { public Processor p1 = new Processor(); public Processor p2 = new Processor(); public CompositeEquip motherBoard = new CompositeEquipment(); motherBoard.addEquip(p1); motherBoard.addEquip(p2); float pr1 = p1.price(); float pr2 = p2.price(); float pr = motherBoard.price();

La méthode main se comporte comme client, crée deux objets simples (de la classe Processor) puis les intègre dans un objet composite appelé motherBoard. Par la suite, elle récupère les prix de chaque composant mais aussi le prix de motherBoard. Elle utilise la méthode price indépendamment de la classe des objets.

...

} Listing 4.1. Exemple de code utilisant le modèle composite Le contexte obtenu. Le modèle Composite permet de définir des hiérarchies de classes décrivant des objets primitifs et des objets composites. Les objets primitifs peuvent être composés en objets plus complexes, qui, à leur tour, peuvent être composés et ainsi de suite récursivement. L’objet client utilise l’objet primitif ou l’objet composite indifféremment. Le niveau client se trouve simplifié car il traite de façon identique les objets primitifs ou composites, il ne gère pas leurs particularités structurelles. Il devient aussi plus simple d’ajouter de nouveaux types de composants qui ne nécessiteront aucun changement des objets clients. En contrepartie, il est difficile d’imposer des contraintes sur la composition des objets. C'est le cas, par exemple, si on souhaite forcer une contrainte qui stipule que deux composants parmi tous les autres ne peuvent être dans un même objet composite.

Raisonnement. Les clients utilisent l’interface de la classe Component pour manipuler les objets de la structure composite. Si l’objet manipulé est une feuille (i.e. primitif), il traite le message de l’objet client directement. Si c’est un composite, il transfère le message à ses composants fils, en effectuant généralement des opérations avant et après ce transfert. Les modèles de conception en relation. Le modèle Décorateur est souvent utilisé avec Composite. Lorsqu’on les combine, on a besoin d’avoir une classe commune entre les deux. Les décorateurs doivent supporter l’interface Composant. La combinaison du Composite avec le modèle Visiteur permet d’encapsuler simplement les opérations qui seraient autrement dispersées dans les classes composantes. Le modèle Itérateur peut être utilisé pour parcourir l’objet composite. Décorateur, Visiteur et Itérateur sont d’autres modèles très utilisés. Les utilisations connues. Le modèle composite est utilisé dans presque tous les systèmes orientés objets. L’exemple le plus connu est celui de la classe View de SmallTalk qui est composite [Gam 95].

5 APPORT ET RAPPORT ENTRE PARADIGMES L’intérêt d’agir sur le produit final a été mis en évidence par le succès de l’approche orientée objets. Les approches ultérieures (composants, modèles de conception, séparations des préoccupations) n’ont fait que confirmer cet intérêt. L’essentiel dans ces approches est la constatation qu’une meilleure représentation et structuration du produit final facilite à la fois son développement et son évolution. Cette dernière étant simplifiée par le fait que sa portée sur le produit soit réduite. Nous décrivons, dans les sections suivantes, l’impact des approches décrites précédemment sur la complexité. A travers les nombreuses approches décrites, nous constatons qu’elles se basent sur quelques principes clés et tentent d’en tirer avantage pour atténuer les problèmes de développement et de maintenance. Nous citons et expliquons ces principes dans ce qui suit. ›

Elimination des dépendances. Il existe diverses formes de couplage et de dépendances entre les éléments d’un système. Par exemple dans un système orienté objets, on peut distinguer la dépendance entre données, la dépendance de définition, la dépendance d’appel, la dépendance fonctionnelle, la dépendance d’héritage, la dépendance d’agrégation, etc. [Men 99, Tak 97]. Lors de l’évolution, le changement qui affecte un élément est susceptible de provoquer un changement au niveau des éléments qui en dépendent, ou dont il dépend. La séparation des éléments de telle sorte à éliminer/réduire les dépendances (et, par conséquent, réduire la complexité) est un atout déterminant dans la réduction de l’effort de maintenance. En effet, un élément qui n’a pas beaucoup de dépendances peut être changé plus facilement. La séparation des préoccupations va au-delà de l’élimination des dépendances, en considérant que si une séparation au niveau conceptuel est opérée, elle permettrait de diminuer le nombre de dépendances. Par exemple, ignorer dans une classe l’aspect sécurité ou synchronisation et les décrire comme entités de première classe à part, réduit la dépendance.

›

Séparer les services communs (pervasives services). Les services communs sont les fonctionnalités qui apparaissent dans beaucoup de systèmes. Ils ont la propriété de ne pas être sujet à l’évolution (ou peu, relativement aux autres fonctionnalités). Par exemple, la gestion de la persistance, la gestion des transactions, le transport des objets, … La séparation des services communs permet d’économiser l’effort nécessaire à leur développement et maintenance. Les services communs ne se limitent pas seulement à ceux d’ordre technique (i.e. proches des plateformes matérielles) mais peuvent aussi être des fonctionnalités métier spécifiques à un domaine donné.

›

Séparer les services stables. Savoir, dès le départ, quelle partie sera sujette à l’évolution et laquelle le sera rarement, est un facteur qui permet non seulement d’augmenter la productivité mais aussi de simplifier l’évolution. Par exemple, dans l’approche orientée objets, les classes abstraites peuvent être utilisées pour

représenter les éléments stables et les classes feuilles pour représenter les parties variables et évolutives. ›

Rendre la séparation persistante. Nous entendons par ce point la possibilité de maintenir présente la séparation opérée entre les éléments dans les différents niveaux d’abstraction (particulièrement ceux concernés par la maintenance). La séparation entre deux entités E1 et E2 n’est pas persistante si, dans un niveau inférieur d’abstraction, il n’est pas possible de séparer clairement ce qui représente E1 de ce qui représente E2. La persistance engendre une meilleure traçabilité, ce qui permet de faciliter le travail de maintenance [Men 99]

›

Anticiper les changements. Si les changements pouvaient être anticipés au moment de la conception, il serait aisé de les inclure moyennant un paramétrage dans le système. Malheureusement, l’expérience nous a montré que beaucoup de changements qui touchent les systèmes, n’ont jamais été imaginés par leurs concepteurs [Ben 00]. Rechercher les méthodes et outils qui permettent d’extraire un maximum de besoins et aussi anticiper les changements est un domaine de recherche prometteur pour la maintenance.

›

Rendre explicite les décisions de conception. Plusieurs propriétés d’un système sont dans l’esprit de leur concepteur. Au fur et à mesure de l’évolution, les décisions de conception et les suppositions faites ne sont plus respectées, car elles sont difficiles à déduire du système [Leh 97]. Les rendre explicite dans le modèle du système, ou dans la documentation, permet d’éviter des incohérences et des incompatibilités.

›

Augmenter le niveau d’abstraction. Il est clair que l’évolution des paradigmes a eu un effet bénéfique sur la réduction des problèmes de l’évolution. Nul ne peut nier que maintenir un programme en langage d’assemblage est une tâche pénible et source d’erreurs par rapport à un programme Java, par exemple. La recherche de nouveaux paradigmes qui augmentent le niveau d’abstraction, tout en permettant de spécifier rigoureusement les fonctionnalités attendues d’un système, reste un des défis du génie logiciel. Actuellement, l’amélioration du niveau d’abstraction se base sur les middlewares (tel que Corba) et les machines virtuelles.

Dans les sections suivantes nous donnons un aperçu sur l’impact des différents paradigmes sur la complexité.

5.1 Impact des paradigmes 5.1.1 Impact de la stabilité/variabilité L’apport de la variabilité pour le processus de développement est consistant [Jaz 00, Dee 03]. En effet, dans cette approche, on anticipe sur les différents besoins des utilisateurs pour leur fournir un produit qui réponde à leur attente. Le fait d’identifier des variantes et d’en tenir compte pendant le développement, permet de réduire certaines opérations de changement à un simple changement de variantes. La stabilité et la variabilité sont deux facettes d’une même notion. Cependant, dans la stabilité, l’accent et mis sur les parties d’un système qui ne changent pas d’une application à l’autre d’un même domaine, ce qui leur permet de rester inchangées et d’être réutilisées. On peut considérer alors certains objets industriels comme des variantes. Dans la variabilité,

l’accent est mis sur la détermination des parties qui peuvent exister en plusieurs variantes pour une même ligne de produit (i.e. la même application). Dans les deux cas, on sépare le système en partie commune et en parties différentes ou pouvant se différencier dans l’avenir. Notons enfin, que la mise en œuvre de la stabilité, se fait moyennant des patrons de haut niveau (patterns) tel que AnyAccount, qui fait ressortir clairement que l'obtention et la gestion d'un compte utilisateur dans divers systèmes présente une stabilité dont on peut tirer profit [Fay 03].

5.1.2 Impact de la réutilisation La réutilisation simplifie le développement et la maintenance à la fois. En effet, parce qu’une partie non négligeable du logiciel existe déjà, tout l’effort de développement peut se focaliser sur la partie réellement nouvelle du logiciel. D’un autre côté, la partie réutilisée, ayant déjà fait ses preuves à travers de multiples changements, devient moins sujette à l’évolution que la partie nouvelle. Notons cependant que, même si un élément est convenable dans un contexte donné, son intégration dans un autre contexte (e.g. contraintes de gestion différentes ou plateforme différente) peut provoquer des incompatibilités et que ces dernières peuvent être des sources de fautes qui engendrent des changements futurs. La réutilisation présente aussi certains inconvénients, qui sont cependant raisonnables comparés aux avantages qu’elle procure. En effet, les éléments réutilisables demandent un effort supplémentaire, aussi bien pour être identifiés et créés que pour être maintenus. Cet effort ne se justifie que si la réutilisation est fréquente et son taux important. Notons enfin qu’il existe une forte relation entre la réutilisation et l’approche basée composants (voir chapitre 3), dans le sens où l’utilisation des composants (avec configuration) représente une forme particulière de réutilisation qui se limite, le plus souvent, au code.

5.1.3 Impact de la MDA L’approche MDA s’articule autour de l’utilisation de modèles qui explicitent la connaissance que le développeur a du domaine considéré et l’indépendance vis-à-vis des plateformes. D’un autre côté, l’espoir d’une automatisation accrue des transformations laisse prévoir des retombées positives sur les processus de développement et de maintenance. En effet, les PIMs ont deux avantages. Le premier est le fait que les personnes responsables de la définition des fonctionnalités sont déchargées de la prise en compte d’une quelconque plateforme et se concentrent sur les règles de gestion. Le deuxième avantage découle du fait que les PIM soit découplés des plateformes (i.e. élimination d’un nombre considérable de dépendances), ce qui signifie que tout changement dans celles-ci n’aura aucun effet sur les PIMs et que le même PIM peut être réutilisé et transformé en plusieurs PSM déployés sur diverses plateformes. Si, par exemple, l’application doit migrer vers une nouvelle plateforme, il suffit de générer un nouveau PSM, puis le code correspondant à la plateforme, plutôt que de migrer le code lui même. Ainsi, l’approche MDA préserve l’investissement, intellectuel et financier, consenti pour le développement des PIM d’une application donnée. Elle permet l’interopérabilité entre plateformes, la portabilité et l’amélioration de la productivité. Les

avantages de l’approche MDA sont nombreux et ont tous un impact sur le développement et la maintenance Nous citons ce qui suit : ›

Réaction rapide aux changements touchant la technologie des plateformes. Les changements consistent à modifier les modèles des plateformes et à opérer d’autres mappings, le PIM demeure intact.

›

Réaction rapide aux changements dans les fonctionnalités métiers. Ces changements touchent le PIM, le reste des modèles demeure inchangé, le PSM et le code doivent être régénérés.

›

Extension de la longévité des systèmes. Dans l’approche classique, la maintenance d’un système dépendant d’une plateforme peut conduire à ce que l’architecture de ce dernier ne puisse plus répondre à de nouveaux besoins. A l’opposé, dans l’approche MDA, les fonctionnalités et l’architecture du système sont découplées. De ce fait, un changement dans l’architecture peut se limiter à une transformation automatique. Des changements radicaux peuvent toucher les fonctionnalités ou l’architecture de manière indépendante, ce qui augmente la durée de vie d’un système.

›

Réutilisation importante. Les mappings et les modèles utilisés dans les transformations peuvent être largement réutilisés. Au fur et à mesure de l’évolution, ces éléments deviennent plus riches et plus stables, de ce fait, développer un nouveau système à partir de ces composants signifie qu’il y aura moins de bogues et moins de futures évolutions à opérer sur le nouveau système.

›

Réduction du coût de maintenance. En plus des réductions qui découlent des points précédents, il faut ajouter le fait que le maintien de la synchronisation entre les modèles et le code permet aux développeurs, ou ceux qui maintiennent le système, d’avoir une vue graphique abstraite qui facilite leur travail, même s’ils ne sont pas familiers avec le système.

›

Réduire la charge du maintien de la documentation. Dans l’approche MDA, les modèles, le code et la documentation sont toujours synchronisés. La documentation étant générée automatiquement, elle reste conforme aux derniers changements opérés sur le système. Ce point est important car la maintenance se base sur la documentation.

›

Diminuer le coût de l’assurance qualité. L’approche MDA permet de découvrir tôt les erreurs par l’utilisation d’outils de test, avant même d’entamer la génération de code. De ce fait, la portée des erreurs est limitée et le coût investi dans l’assurance de la qualité du système est réduit. De plus, la facilité de communication entre les développeurs (étant concernés par une tâche de modélisation et non plus de codage qui est une source d’erreurs) améliore la qualité de manière significative.

Poole va au-delà de ces avantages, en considérant que l’expérience acquise en développant et en déployant des systèmes basés sur les métadonnées et les modèles objets extensibles, conduira, en fin de compte, au développement de systèmes capables d’interpréter directement les modèles et modifier leurs comportements plutôt que d’opérer des mappings pour les implémenter [Poo 01]. Par exemple, la publication d’une nouvelle version d’un modèle dans un référentiel (semblable à ceux de Corba) provoque une réaction dans les systèmes logiciels environnants, qui ajustent dynamiquement leur comportement pour refléter ce changement. Il ressort de ce qui précède que, dans l’approche MDA, gérer la complexité est un aspect central. Cependant, les objectifs visés sont difficiles à atteindre. En effet, selon Thomas [Tho 04], la définition des métamodèles est difficile et requiert une grande précision mentale, de même, il n’est pas trivial, voire impossible, de supporter et faire évoluer sémantiquement

des PSMs vu la complexité des plateformes actuelles et leur changement constant. Thomas résume la difficulté par ce paragraphe: «Furthermore, even if through some Herculean effort we could produce such a PSM, it would no sooner be available than the vendor’s next release would render it outdated. We have decades of experience building automated code generators for compilers and yet still find it challenging to build a code generator for Itanium processor, let alone for an Itanium running a particular vendor’s operating system, Java virtual machine, Java 2 Enterprise Edition, and middleware. The accidental complexity of the stack makes a comprehensive PSM virtually impossible. Some MDA proponents respond that they generate the code from the model and then let the developers deal with the remaining specifics of platforms, libraries, and legacy interfaces. This is a nightmare because now the poor developer, misled by the “all you need is UML” hype, is stuck having to debug and develop code that a tool generated. They are forced to dive deep into the most difficult part of the development cycle—using a specific-platform application programming interface».

5.1.4 Impact de la séparation avancée des préoccupations Le point de départ de la séparation des préoccupations est la séparation entre les fonctionnalités qu’un logiciel accomplit et les préoccupations qui contraignent le déroulement de ces fonctionnalités. L’apport de ces approches est concret comme nous le montre les points suivants : ›

Elimination des dépendances. Les dépendances vont de paire avec l’enchevêtrement. La séparation des préoccupations élimine l’enchevêtrement des préoccupations avec les fonctionnalités mais réduit aussi l’enchevêtrement entre les différentes préoccupations. Par conséquent, les dépendances diminuent et le changement d’une partie aura un effet réduit et bien localisé sur les autres. Par exemple, le fait de séparer le code qui s’occupe de la synchronisation dans un aspect ou une interface CF permet de supprimer les dépendances de définition, les dépendances d’héritage, les dépendances de contrôle, etc. Notons aussi qu’il y a une inversion des dépendances, dans le sens où les fonctionnalités n'ont plus connaissance de l’existence des préoccupations (e.g. les fonctionnalités n’appellent pas les méthodes des préoccupations). Par contre, les préoccupations interceptent les méthodes des fonctionnalités et ont, de ce fait, une dépendance vis à vis de leur signature.

›

Séparation des services communs. Les préoccupations constituent aussi des services communs car elles entrecoupent les diverses fonctionnalités. Leur séparation facilite l’évolution de par le fait qu’elle élimine l’éparpillement du code et qu’un changement qui affecte ces services aura un effet réduit sur le reste de l’application. Par exemple, si on souhaite changer la stratégie de sécurité ou de synchronisation, il suffit d’opérer ces changements au niveau des préoccupations.

›

Maintien de la séparation persistante. Le code exécutable est généré des fonctionnalités et préoccupations par des opérations de tissage et de composition. Ces opérations recréent l’enchevêtrement et détruisent donc la séparation. Si la maintenance est opérée sur le code exécutable (e.g. mise à jour dynamique des bytecodes Java), les avantages ne sont pas tangibles. Par contre, si l’évolution est opérée sur le code source et les niveaux qui lui sont supérieurs, on peut considérer que la séparation est persistante, et, de ce fait, l’impact d’un changement sur l’application et nettement plus localisé que dans le cas d’une application orientée

objets classique. Les approches de séparation des préoccupations auront un impact meilleur si la séparation des préoccupations au niveau du code de l’application, que l’on souhaite maintenir, reflète la séparation de ces derniers au niveau de la conception. ›

Doter le système de moyens d’adaptation dynamique. Certaines implémentations particulières de la séparations des aspects (e.g. plate forme Java Aspect Component [Jac 04]) traitent des changements non anticipés, en offrant la possibilité d’ajouter dynamiquement des aspects. Cette possibilité est importante car elle suppose une persistance des aspects et donc une facilité de les ajouter ou de les retirer ce qui engendre un changement dans l’exécution de l’application (sans effet notable sur son code).

Les approches par séparations des préoccupations ont un impact indéniable sur la maîtrise de la complexité par rapport à l’approche orientée objets classique. Malheureusement, l’étude de la quantification de cet impact n’est pas aisée, vue l’évolution rapide des approches. Signalons cependant que des tentatives ont eu lieu dont principalement celles décrites dans [Wal 99, Mur 99]. La relation entre préoccupations est sujette aux changements que ce soit pendant ou après le développement du logiciel. On peut alors parler d’un certain enchevêtrement entre les préoccupations. Dans ce cadre, l’approche hyperespace, offre la particularité de permettre l’identification, l’encapsulation et l’intégration de n’importe quel type de préoccupations et à n’importe quel moment du cycle de vie du logiciel. L’ensemble des préoccupations pertinentes sera toujours dynamique et procure au développeur une certaine souplesse dans l’accomplissement de sa tâche.

5.1.5 Impact des composants logiciels Le point fort de l’approche vient du fait que la séparation entre composants est nette et persistante. L’approche apporte un avantage certain aussi bien pour le développement (grâce à une réutilisation importante) que pour l’évolution. Elle est particulièrement souple lorsque les changements ne sont pas très innovants sur le plan architectural et consistent à remplacer un composant par un autre ou à reconfigurer partiellement l’architecture et les connexions entre les composants. Par contre, la granularité de niveau composant n’est pas toujours convenable. La mise à jour dynamique ne peut s’accommoder de celle-ci. D’un autre côté, l’absence de la séparation des préoccupations laissent apparaître un éparpillement du code de ces dernières sur plusieurs composants. Ce point est crucial car, en cas de changement, il peut engendrer des remplacements multiples de composants. Les points qui suivent détaillent certains aspects des composants [Vit 03, Vit 03a]. ›

Elimination des dépendances. On peut parler de deux types de dépendances : les dépendances intra composant et les dépendances inter composants. L’approche basée composants est innovatrice concernant les secondes. En effet, les dépendances sont déclarées explicitement, ce qui simplifie grandement leur modification, qui peut aussi se faire dynamiquement.

›

Séparer les services communs. L’extraction des services communs est à la charge du développeur. Si elle est bien faite, en d’autres termes si les services sont encapsulés dans un ou plusieurs composants indépendants des autres parties de l’application, elle aura un effet bénéfique sur l’évolution. Pour que la production des composants soit rentable, il faut que chaque composant offre les fonctionnalités couramment utilisées. Cette contrainte économique à un double

effet : d’un côté, les composants qu’on réutilise souvent sont forcément des composants qui offrent des services communs, d’un autre côté, la pression peut être importante au point où le composant se retrouve doté de fonctionnalités qui sont rarement utilisés (i.e. on cherche à couvrir les besoins d’un nombre maximum d’utilisateurs). Même si les fonctionnalités supplémentaires n’ont pas d’influence sur l’évolution, elles dégradent les performances des applications. ›

Séparer les services stables. Le cycle répété de réutilisation et d’adaptation des composants finit par les rendre stables. La réutilisation des composants lors du développement suppose que le développeur arrive à identifier les services qui correspondent à un composant. Si c’est le cas, il identifie de facto les parties stables de son système.

›

Augmenter le niveau d’abstraction. L’approche par composants a introduit deux niveaux de description : description intra composant et description inter composants. Cette dernière présente un niveau d’abstraction appréciable qui permet une reconfiguration et un déploiement facile des applications.

5.1.6 Impact des modèles de conception Un modèle de conception est une solution, à un problème, qui a fait ses preuves (i.e. testée, corrigée et améliorée sur plusieurs cycles). La probabilité d’avoir à changer l’idée de ce dernier est faible et, par conséquent, les systèmes qui sont développés à base de modèles de conception présentent une stabilité relative vis à vis des systèmes orientés objets classiques. Par ailleurs, les modèles de conception ont un impact sur le processus de développement, car ils représentent des solutions sur lesquelles il y a eu un consensus et sont, par conséquent, un moyen de communication et d’échange. Vu l’importance de ces derniers dans tout processus de développement et de maintenance, simplifier la communication et l’échange peut avoir un impact important sur les processus. Les modèles de conception ont des structures connues et même un nommage spécifique des éléments qui y interviennent (e.g. class Component, addComponent, ... pour le modèle Composite), de ce fait, la compréhension du code source d’un système est simplifiée. Par transitivité, faciliter la compréhension permet de maîtriser la complexité. Une bonne compréhension des modèles de conception peut être un moyen qui aide le développeur à extraire les besoins et évite l’introduction de nouvelles erreurs lors de la maintenance. Cet avantage vient du fait qu’un modèle est un tout et que chaque élément qui le compose joue un rôle spécifique. Pourvu que le développeur applique une approche méthodique, il parviendra à déterminer tous les éléments, de son application, impliqués dans le modèle. De même, lors de la maintenance, la connaissance des éléments, des tâches qu’il faut entreprendre et l’impact d’un changement sur le reste du modèle de conception sont autant d’éléments qui facilitent l’évolution et évitent l’oubli de certains changements. Les modèles de conception peuvent exister à différents niveaux d’abstraction et ont un impact sur l’évolution qui touche chacun de ces niveaux. Cependant, la traçabilité n’est pas garantie. En effet, le passage d’un niveau d’abstraction donné à un niveau inférieur peut détruire la structure du modèle de conception, s'il n’y a pas de concepts et mécanismes

adéquats pour son mapping. De plus, la composition des modèles de manière libre peut créer un enchevêtrement difficile à démêler lors de la maintenance. Finalement, on peut conclure que les modèles de conception sont un moyen précieux pour capturer et communiquer la connaissance et l’expérience acquise dans le domaine du développement des logiciels. Ainsi, ils conduisent à l’amélioration de la qualité des logiciels et de la productivité et permettent aux concepteur d’éviter certains pièges du développement et de la maintenance que d’autres ont connus.

5.2 Rapport entre paradigmes La séparation des préoccupations est un principe ancien qui a été longuement adopté comme démarche dans l’ingénierie des systèmes logiciels [Par 72]. Son objectif est de faire correspondre des représentations indépendantes aux préoccupations indépendantes. Il résulte alors une décomposition du logiciel en parties qui sont plus facilement manipulables et compréhensibles, d’où un effet important sur la maîtrise de la complexité pendant le développement et la maintenance. Actuellement, ce principe est vu sous un angle nouveau, celui de chercher d’autres formes de décompositions qui sont, éventuellement, simultanées. Dans un sens plus large, la maintenabilité, la configurabilité, l’adaptabilité, la réutilisabilité, ... sont aussi des préoccupations. Les trois approches, présentées dans les chapitres 2, 3 et 4, sont des approches de séparation des préoccupations. Les composants maintiennent leurs fonctionnalités séparées des autres composants, des implémentations ainsi que des préoccupations de déploiement. Quant aux modèles de conception, ils permettent une forme particulière de séparation entre les fonctionnalités nouvelles d’une application et les fonctionnalités récurrentes qui reviennent dans diverses situations et domaines d’application.

5.3 Vers une hybridation des paradigmes La programmation orientée composants et la programmation orientée aspects s’unissent autour d’un certain nombre de points communs comme la notion de modularité du code. Cependant, elles divergent sur certains points : l’approche orientée composants souffre des mêmes problèmes que l’approche objet. En effet, on retrouve le même enchevêtrement par mélange et dispersion au niveau composant. L’approche orientée aspects, travaillant principalement au niveau objet, ne permet pas une réutilisabilité et une modularité optimale. Une combinaison des deux approches semble prometteuse. Dans les approches basées composants, la modularité est incarnée par les composants euxmêmes. En effet, les préoccupations métier sont incarnées dans différents composants offrant chacun un ensemble de services, et suffisamment indépendants les uns des autres pour pouvoir être déployés dans différents contextes sans modification. Dans les approches de séparation des préoccupations, la modularité s’exprime à travers la séparation des préoccupations, le code métier est indépendant du code technique, et chaque partie peut

être développée, maintenue et améliorée en parallèle. Cependant, la modularité est plus forte dans l’approche à base de composants parce qu’elle est persistante. Si on considère les approches de séparation des préoccupations elles-mêmes, on constate une diversité dans les concepts utilisés, de ce fait, leur comparaison dans le but de trouver un modèle de séparation unifié représente un objectif ambitieux [Mes 04a]. La combinaison des modèles de conception avec d’autres paradigmes est intéressante. Jusqu’à présent, les travaux menés conjointement sur la séparation des préoccupations et les modèles de conception se sont orientés vers l’implémentation de ces derniers par séparation des préoccupations [Cha 02, Mul 03, Nat 01]. Nous prenons actuellement une direction différente, dans laquelle nous utilisons les modèles de conception comme moyen de comparaison des approches de séparations des préoccupations. La comparaison peut être d’ordre qualitatif ou quantitatif (complexité, évaluation temporelle empirique couvrant les approches et les environnements support).

partie à revoir ****Dans ce chapitre, nous avons présenté les nouvelles approches basées produit de l’évolution. Nous avons passé en revue leurs principes, les concepts qu’elles utilisent ainsi que l’apport de chacune face à l’évolution. Les approches actuelles partagent des points communs dans le sens où elles cherchent à donner au produit final une organisation de meilleure qualité pouvant simplifier les changements et limiter leurs effets. Parmi les caractéristiques importantes de ces approches notons : ›

L’amélioration du niveau d’abstraction dans le produit final de sorte que chaque préoccupation de haut niveau ait un correspondant unique (ou un nombre limité et gérable de correspondants).

›

La réduction de l’éparpillement.

›

La réduction de l’enchevêtrement.

Notons enfin, qu’actuellement, il y a une tendance à la combinaison des différentes approches, aussi bien celles portant sur le produit que celles portant sur le processus, et on s’attend à des retombées positives de cette combinaison [Mes 05]. Dans le chapitre suivant nous décrivons comment l’évolution est mise en œuvre par les processus biologiques.

6 TENDANCES FUTURES Ingénierie dirigée par les modèles Grid computing Autonomique computing et les approches bioinspirées

REFERENCES [Bas 96]

P. G. Bassett, “The 3 paradoxes” in Focus on Reuse, Object magazine, April 1996, pp. 59-63

[Bel 76]

L. A. Belady and M. M. Lehman, A Model of Large Program Development, IBM Systems Journal, Vol. 15, No. 1, 1976, pp. 225-252.

[Ben 00]

K. H. Bennett, et V. T. Rajlich, “Software Maintenance and Evolution : A Roadmap”, in Finkelstein A. (ed.), “The future of Software Engineering”, 22nd ICSE, Limerck, Ireland, June 2000, pp 73-87.

[Béz 04]

J. Bézivin, Sur les principes de base de l’ingénierie des modèles, L’objet, 10/2004, pp. 145-156

[Boo 93]

G. Booch, Object-oriented analysis and design with applications, 2 Benjamin Cummings, 1993.

[Cop 98]

J. Coplien et al., Commonality and variability in software engineering, IEEE Software, Vol. 15, No. 6, November 1998, pp 37-45.

[Dee 03]

S. Deelstra & al, Model Driven Architecture as Approach to manage Variability in Software Product Families, Proceedings of the Workshop on Model Driven Architecture : Foundations and Applications, MDAFA 2003, pp. 109-114

[Dvo 94]

J. Dvorak, Conceptual Entropy and its Effect on Class Hierarchies, Computer, Vol. 27, No 6, June 1994, pp. 59-63

[Fay 01]

M.E. Fayad, and A. Altman, Introduction to software stability, Communications of the ACM, Vol. 44, No. 9, September 2001.

[Fay 02]

M.E. Fayad, How to Deal with software stability, Communications of the ACM, Vol. 45, No. 4, April 2002, pp 109-112.

[Fra 95]

W. B. Frakes & C. J. Fox, Sixteen Questions About Software Reuse, CACM, Vol. 38, No. 6, pp 57-63, 1995

[Gur 01]

J. V. Gurp et al, On the Notion of Variability in Software Product Lines, in Proceedings of WICSA 2001, August 2001.

[Hon 98]

K. De Hondt, A Novel Approach to Architectural Recovery in Evolving ObjectOriented Systems, Ph.D. Dissertation, Computer science department, VRIJE Univesiteit, Brussel, December 1998.

[J2E 04]

Java2 Enterprise Edition, The official homepage of J2EE, java.sun.com/j2ee, Sun Microsystems, 2004.

[Jac 92]

I. Jacobson, et al., Object-Oriented Software Engineering - A Use Case Driven Approach, Addison-Wesley, 1992.

[Jaz 00]

M. Jazayeri et al., Software Architecture for Product Families: Principles and Practice, Addison-Wesley, 2000.

[Kem 99]

C. F. Kemerer and S. Slaughter, An Empirical Approach to Studying Software Evolution, IEEE Transactions on Software Engineering, Vol. 25, No. 4, 1999, pp. 493-509.

[Kim 05]

M.T. Kimour et D. Meslati, Derving objects from uses cases in real time embedded systems, Journal of Information and Software Technology, Volume 47, Issue 8, Elsevier edition, June 2005, pp. 533-541.

[Kla 04]

Klasse Objecten, www.klasse.nl/english/mda/mda-introduction.html, Objecten Inc, Lastly accessed: May 15, 2004

nd

edition,

Klasse

[Kle 03]

Kleppe A, Warmer J, Bast W, MDA Explained - The Model Driven Architecture: Practice and Promise, Addison-Wesley, 2003

[Kuch 04]

P. Kuchana, Software acrchitecture design patterns in Java, Auerbach Publications, CRC Press, 2004

[Leh 97]

M.M.Lehman, Laws of Software Evolution Revisited, Workshop EWSPT96, 1997.

[Li 99]

X. Li, A Survey of Schema Evolution in Object-Oriented Databases, Proceedings st of the 31 International Conference on Technology of Object-Oriented Language and Systems, pp. 362, September 1999.

[Mcc 97]

C. McClure, Reuse Engineering: Adding Reuse to the Software Development Process, Prentice-Hall, 1997

[Mel 04]

S. J. Mellor & al, MDA Distilled: Principles of Model Driven Architecture, Addison Wesley, March 2004

[Men 99]

T. Mens, A Formal Foundation of Object Oriented Software Evolution, Phd dissertation, Computer science department, VRIJE Univesiteit, Brussel, 1999.

[Mil 01]

J. Miller, J. Mukerji, Model Driven Architecture (MDA), Document number ormsc/2001-07-01, Architecture Board ORMSC, July 9, 2001. http://www.omg.org/mda/ presentations.htm

[Mil 03]

J. Miller, J. Mukerji, http://www.omg.org/mda

[Mub 04]

A. Mubin, Constructing MDA-based Application using Rational XDE for .NET, March 2004

[Omg 04]

Model-Driven Architecture, the official homepage, www.omg.org/MDA

[Opd 92]

W. Opdyke. Refactoring object-oriented frameworks. PhD thesis, University of Illinois at Urbana-Champaign, 1992.

[Pfl 01]

S. L. Pfleeger, Softawre Engineering: Theory and Practice, Second Edition, Prentice-Hall, 2001.

[Poo 01]

J. D. Poole, Model driven architecture: Vision, Standards and Emerging Technologies, Position paper in European Conference on Object Oriented programming, Ecoop, 2001.

[Sie 01]

J. Siegel, Developing in OMG’s Model-Driven Architecture: White paper Revision 2.6, Object Management Group, November 2001.

[Swe 04]

SWEBOK, Guide to the software engineering body of Knowledge, 2004 Version, IEEE Computer Society, 2004

[Tak 97]

A. Taking et P. Grubb, Software maintenance concepts and practice, International Thomson Computer Press, 1997.

[Tho 04]

D. Thomas, MDA : Revenge of the modelers or UML utopia, IEEE Software, May/June 2004, pp. 22-24.

[Yod 03]

J. W. Yoder et R. Johnson, The Adaptive Object-Model Architectural Style, 2003

[Aks 97]

M. Aksit & L. Bergmans, Software Evolution Problems in Case of Inheritance and Aggregation Based Reuse, Tutorial of the Trese Group, http://trese.cs.utwente.nl/composition_filters/, Last visited Juin 2004.

[Aks 98]

M. Aksit, B. Tekinerdogan, Aspect-Oriented Programming Using Composition Filters, ECOOP’98 Workshop Reader, Springer Verlag, pp. 435, July 1998.

[Aks 98a]

M. Aksit & B. Tekinerdogan, Solving the Modeling Problems of Object-Oriented Languages by Composing Multiple Aspects Using Composition Filters, AOP’98 workshop position paper, 1998.

[Ale 79]

C. Alexander, The Timeless Way of Building. New York: Oxford University Press, 1979.

[App 00]

B. Appleton, Patterns and Software: Essential Concepts and Terminology, World Wide Web http://www.enteract.com/~bradapp/, last modified 14 February 2000.

[Ber 01]

L. Bergmans & M. Aksit, Composing Crosscutting Concerns Using Composition Filters, Communications of the ACM, Vol. 44, No. 10, pp. 51-57, October 2001.

MDA

Guide

version

1.0.1,

OMG,

June

2003.

[Ber 94]

L. Bergmans, Composing Concurrent Objects, Ph.D. thesis, University of Twente, 1994.

[Ber 94a]

L. Bergmans, The Composition Filters Object Model, Dept. of Computer Science, University of Twente, 1994.

[Bru 04]

E. Bruneton et al., The Fractal Component Model, ObjectWeb Consortium, France Telecom et INRIA, Février 2004, http ://fractal.objectweb.org

[Cha 02]

S. Chan, Design Pattern- Concept ands application, COMP414, Object-Oriented Technologies, Reading Project Report., The Hong Kong polytechnic university, 29 April 2002.

[Cop 96]

J. Coplien, Software patterns, SIGS Books & Multimedia, 1996.

[Dou 04]

L. Doug, Design patterns for avionics control systems, http://g.oswego.edu/dl, 2004.

[Dow 01]

J. Dowling & V. Cahill, The K-Component architecture meta-model for selfadaptive software, Reflection 2001, The third international conference on metalevel architecture and separation of crosscutting concerns, Kyoto, Japan, September 2001.

[Gam 95]

E. Gamma et al., Design Patterns: Elements of Reusable Object-Oriented Software, Addison Wesley, Massachusetts, 1995.

[Gro 00]

J. Gosling, et al., The Java Language Specification (Second Edition), AddisonWesley Publishing, 2000.

[Gup 96]

D. Gupta et al., A Formal Framework for On-line Software Version Change. Transactions on Software Engineering, 22(2):120-131, February 1996.

[Hic 01]

M. Hicks. Dynamic Software Updating. PhD thesis, Department of Computer and Information Science, University of Pennsylvania, August 2001.

[Ho 91]

Ho W. W. et R. A. Olsson. An Approach to Genuine Dynamic Linking. Software Practice and Experience, Vol. 21, No. 4, pp. 375-390, April 1991.

[Hyp 05]

Hyper/J web site: http://www.research.ibm.com/hyperspace/

[Jac 04]

Jac objectweb site. http://jac.objectweb.org.

[Kic 01]

G. Kiczales et al, An Overview of AspectJ In Proc. of ECOOP, Springer-Verlag, 2001.

[Kic 97]

G. Kiczales et al., Aspect Oriented Programming. In Proc. of European Conference on Object-Oriented Programming (ECOOP), Lecture Notes in Computer Science Vol. 1241, pp. 220-242, 1997. http://eclipse.org/aspectj

[Koo 95]

P. Koopmans, On the Design and Realization of the Sina Compiler, MSc. thesis, Dept. of Computer Science, Univ. of Twente, 1995.

[Lad 03]

R. Laddad, AspectJ in action: Practical Aspect-Oriented Programming, Manning Publications Co., 2003.

[Lee 83]

I. Lee, DyMOS: A Dynamic Modification System. PhD thesis, Department of Computer Science, University of Wisconsin, Madison, April 1983.

[Li 99]

X. Li, “A Survey of Schema Evolution in Object-Oriented Databases”, Proceedings st of the 31 International Conference on Technology of Object-Oriented Language and Systems, pp. 362, September 1999.

[Mal 00]

S. Malabarba, et al., Runtime support for type-safe dynamic Java classes. In Proceedings of the Fourteenth European Conference on Object-Oriented Programming, June 2000.

[Mar 02]

R. Marvie et M. C. Pellegrini, Modèles de composants, un état de l’art, RSTIL’objet, No 8, 2002, pp 61-89.

[Men 97]

A. Mendhekar, et al., RG: A Case Study for Aspect-Oriented Programming, Xerox PARC, Palo Alto, CA. Technical report SPL97-009 P9710044, February, 1997.

[Mes 04a]

D. Meslati et al., La composition de filtres et AspectJ : Une Comparaison conceptuelle, 8ème Maghrebian Conference on Software Engineering and Artificial Intelligence, Sousse, Tunisie, 9-12 Mai 2004

[Mes 05]

D. Meslati et al., From composition filters to AspectJ: A PSM to PSM transformation, proceedings of the International Symposium on Programming and System, ISPS'2005, Algiers May 9-11, 2005.

[Mor 97]

M. Morrison, et al., Java (Second Edition), MacMillan Computer Publishing, 1997.

[Mor 99]

G. Morrisett, et al., TALx86: A realistic typed assembly language. In Second Workshop on Compiler Support for System Software, Atlanta, May 1999.

[Mul 03]

R. Mulls, AspectJ Cookbook: Implementing Creational Object Oriented Design Patterns, Chapter 17, pp 175-187.O’Reilly edition 2003.

[Mur 99]

G. C. Murphy et al, Evaluating Emerging Software Development Technologies: Lessons Learned from Assessing Aspect-Oriented Programming, IEEE Transactions on Software Engineering, pp. 438-455, July/August 1999.

[Nat 01]

N. Natsuko, K. Tomoji, Implementing Design Patterns Using Advanced Separation of Concerns, workshop on Advanced Separation of Concerns in Object Oriented Systems, OOPSLA, 2001.

[Oss 99]

H. Ossher & P. Tarr, Multi-Dimensional Separation of Concerns using Hyperspaces. IBM Research Report 21452, April, 1999.

[öst 03]

D. österberg & J. Lilius, Rethinking Software Updating: Concepts for Improved Updatability, TUCS Technical Report No 550, TUCS Turku Center for Computer Science, Åbo Akademi University, Finland, Sept. 2003.

[Par 72]

D. L. Parnas, On the criterea to be used in decomposing systems into modules. CACM, Vol. 15, No. 12, Dcember, 1972.

[Pes 00]

D. Pescovitz, Monsters in a box, Wired, 8(12):341-347, 2000.

[Riv 04]

M. Riveill & P. Merle, Programmation par composants, Techniques de l’ingénieur, Traité Informatique, H 2 759, 2004.

[Szy 96]

C. Szyperski & C. Pfister, WCOP’96 Workshop report, Ecoop’96, June 1996, pp. 127-130.

[Tar 99]

P. Tarr et al, N degrees of separation: Multi-dimensional separation of concerns. In st Proc. of the 21 International Conference on Software Engineering, pp. 107-119, 1999.

[Vit 03]

P. Vitharana, Risks and Challenges of Component-Based Software Development, CACM, August 2003, Vol. 46, N°8.

[Vit 03a]

P. Vitharana et al, Design, Retrieval and Assembly in Component-Based Software Development, CACM, August 2003, Vol. 46, N°11.

[Wal 99]

R. Walker et al, An Initial Assessment of Aspect-Oriented Programming. Proc. of st the 21 International Conference on Software Engineering, pp. 120-130, 1999.

[Gel 94]

M. Gell-Mann, The Quark and the Jaguar: Adventures in the Simple and the Complex. New York: W.H. Freeman and Company, 1994.