|
|
|
|
|
|
B5- Représentation des classes-feuilles et des méthodes-feuilles
Les classes-feuilles (ou finales) sont des classes qui n'ont pas et ne peuvent pas avoir de sous-classes. En C++, les classes (et les méthodes) sont réputées finales à moins d'être déclarées virtual. En Java, elles sont déclarées avec le modifiant final:
public final class FinalClass
extends
SuperClass {
}
UML ne précise aucun moyen de signaler qu'une classe est final, pas plus pour signaler qu'une méthode l'est aussi. Pour ce qui est des méthodes, UML permet l'ajout d'un préfixe devant une méthode pour signaliser qu'une méthode est final. Ainsi, on peut ajouter le préfixe 'F' devant toute méthode finale. Mais comme il n'existe aucun préfixe standard, un modeleur pourrait très bien utilisé un autre préfixe que 'F' pour signaler la finalité d'une méthode.
Par contre, les préfixes ne sont pas permis comme modifiants de classe, il faut donc utilisé un stéréotype, par exemple <<final>>, pour signaler qu'une classe est final. Il y a cepandant deux problèmes de taille: primo, un concept similaire (la finalité d'une classe et d'une méthode) n'est pas modélisé de façon similaire, ce qui viole le principe 4. Et secundo, UML ne permet qu'un seul stéréotype par classe. Ainsi, si une classe est à la fois une exception et une classe finale (ce qui peut arriver), il faut choisir entre le stéréotype <<exception>> et <<final>>! À moins de créer un nouveau stéréotype, <<final exception>>, qui hériterait de <<exception>> et <<final>> (puisque l'héritage multiple est permis sur les stéréotypes en UML), mais on imagine le nombre de stéréotypes à implanter pour soutenir tous les concepts (en fait, si on a à soutenir n concepts indépendants, on aurait à créer n! stéréotypes pour soutenir toutes les combinaisons de concepts, voir point 12).
On pourrait modéliser une classe finale à l'aide d'un
petit triangle inversée apposé au-dessous d'une classe, comme
indiqué sur la figure suivante:
Figure 1 : Une classe finale (non sous-classable) |
On voit immédiatement que le triangle occupe la place qu'aurait
occupé la flèche de l'héritage; il est donc impossible
graphiquement qu'une classe finale soit sous-classée. Mais
cela est heureux, puisque de toute façon une classe finale ne peut
être sous-classée par définition. Ce formalisme
répond donc au principe 3: tout concept impossible syntaxiquement
devrait être impossible à représenter graphiquement.
Figure 2: On ne peut hériter d'une classe finale. |
Ce même triangle inversée pourrait servir de préfixe devant les méthodes finales (au lieu du 'F'). D'abord, UML permet l'ajout d'un préfixe (quel qu'il soit, même sous forme d'icône), donc cet ajout est conforme à UML. Ensuite, comme le triangle inversée est aussi utilisée pour signaler la finalité d'une classe, ce formalisme respecte le principe 4 : des concepts similaires devraient être représentés graphiquement de façon similaire.
Évidemment, lorsqu'une classe est finale, le préfixe final
est redondant devant les méthodes, puisque toute méthode
d'une classe finale est également finale (par définition,
si une classe ne peut être sous-classée, aucun de ses méthodes
ne peut être overridden). Ainsi, dans le diagramme suivant,
le cas de gauche est redondant, et peut toujours être ramené
au cas de droite.
Si les méthodes privées étaient modélisées
(comme en UML traditionnel), alors le cas d'une méthode à
la fois privée et finale serait toujours redondant (puisque qu'une
méthode qui n'est pas visible de ses sous-classes ne sera forcément
pas overridden par la sous-classe. Ici encore, UML a choisi de modéliser
un concept inutile (la visibilité privée d'une méthode),
et ne pas modéliser un concept essentiel (la finalité d'une
méthode).
-final et protégé?
B6- Representation des classes et méthodes abstraites
Un autre concept qui est similaire dans le modèle objet, et qui est modélisé de manière différente dans UML, est l'abstraction des classes et des méthodes. En UML, un classe abstraite est représentée en mettant en italique son nom (figure 1); les méthodes abstraites elles sont identifiables grâce au préfixe 'A' devant le nom de la méthode; à noter qu'une méthode en italique représente une méthode héritée. Encore un fois, cette notation viole le principe 4 : un concept similaire (l'abstraction d'une classe et d'une méthode) n'est pas modélisé de façon similaire.
Comment pourrait-on modéliser une classe abstraite? Comme
le concept d'abstraction s'oppose au concept de finalité, pourquoi
ne pas le modéliser de façon opposée, c'est-à-dire
à l'aide d'un triangle imbriquée (plutôt qu'inversée).
Figure 1: classe concrète héritant d'une classe abstraite |
Figure 2: classe abstraite héritant d'une classe abstraite |
Il y a plusieurs avantages à ce formalisme. D'abord, le triangle imbriquée suggère que la classe n'est pas complète (puisque la boîte n'est pas complète, ce qui est vrai : une classe abstraite est une classe qui n'est pas complète et qui ne peut donc pas être instanciée et directement utilisé. Ensuite, le triangle imbriquée laisse suggérer que la classe s'attend à être sous-classée, puisque que le triangle imbriquée est propice à ''recevoir'' un lien d'héritage. Une classe abtraite peut ne pas avoir de sous-classes, mais en général elle est abstraite justement parce qu'elle sert de superclasse à des classes concrètes, ou à d'autres classes abstraite. Enfin, on voit facilement qu'il est impossible de dessiner une boîte qui ait à la fois un triangle inversée et imbriquée. C'est encore une fois heureux, puisque qu'il est impossible dans le modèle objet qu'une classe soit à la fois abstraite et finale. En effet, comme une classe abstraite est une classe conçu pour être sous-classée, comment pourrait-on en même temps lui interdire d'être sous-classée? D'ailleurs, une classe déclarée à la fois abstraite et finale ne compile pas. On voit que ce formalisme, contrairement à l'approche traditionnelle de UML, répond au principe 3: tout concept impossible syntaxiquement devrait être impossible à représenter graphiquement.
Comment maintenant noter les méthodes abstraites? Nous avons
vu au point précédent qu'on pouvait utiliser le triangle
inversée comme préfixe pour indiquer la finalité d'une
méthode. Pourquoi ne pas utiliser le triangle ordinaire pour
noter l'abstraction d'une méthode, comme dans le diagramme suivant:
Figure 1 : Les méthodes abstraites pourraient elles aussi être modélisées par un triangle comme préfixe. |
Comme un triangle ne peut être à la fois inversée et l'endroit, le modèle objet interdit qu'une méthode soit à la fois abstraite et finale. En effet, si une méthode est abstraite, c'est-à-dire non-définie (pas de code associé à la méthode), c'est précisement pour permettre à une sous-classe de implémenter la méthode abstraite. Comment alors lui interdire l'override? D'ailleurs, les compilateurs interdisent l'emploi simultané de final et de abstract sur une même méthode. Encore une fois, ceci respecte le principe 3: tout concept impossible syntaxiquement devrait être impossible à representer graphiquement.
Il y a aussi une autre contrainte associé au modèle objet: lorsqu'une classe contient au moins une méthode abstraite, celle-ci doit obligatoirement être déclaré abstraite. En effet, comment pourrait-on instancier un objet d'une classe si celle-ci contenait une méthode abstraite? Comment l'objet réagirait à l'appel d'une méthode si aucun instruction n'y est associée?
Cette fois-ci, autant avec la nouvelle approche qu'avec le UML traditionnel,
il est possible de modéliser une classe concrète qui contient
une méthode abstraite, comme le montre le figure suivante. C'est
à l'outil de génie logiciel d'interdire cette situation.
Une classe abstraite peut contenir des méthodes abstraites, concrètes ou finales. |
Une classe concrète ne peut pas contenir de méthodes abstraites. |
Toutes les méthodes d'une classe finale sont implicitement finales; celle-ci ne peut pas contenir de méthode abstraite. |
À tout le moins, il est plus facile avec la nouvelle approche d'identifier un modèle non-valide: tout modèle avec une classe qui contient une méthode préfixée d'un triangle (méthode abstraite) et qui n'est pas elle même identifée par un triangle imbriquée erend le modèle invalide.
B7- Représentation des classes et des interfaces
En UML, un interface peut être représenté de deux façons: soit par une boîte avec le stéréotype <<interface>> , soit à l'aide d'une petite bulle. Aucune de ces méthodes n'est satisfaisante.
UML utilise un peu trop souvent le mécanisme des stéréotypes pour servir de rustine (de patch) à un concept qui auraît dû être bien modélisé dès le départ. L'utilisation du stéréotype <<interface>> est inacceptable pour les raisons données au point A12. Par exemple, si un interface sert d'exception (ce qui arrive souvent, pensons au Throwable de java.lang), il nous faudra redéfinir un nouveau stéréotype héritant à la fois d'interface et d'exception.
La deuxième façon est peut-être pire encore.
D'une part, un interface et une classe dont toutes les méthodes
sont abstraite sont deux concepts très voisins: comment accepter
qu'ils soient modéliser de façon si différente.
Ensuite, il n'y a pas de place pour placer les méthode d'interface
à l'interieur de la bulle: UML les place en dessous, ce qui en fait
un peu du texte flottant. Dans l'utilisation normale d'un atelier
de génie logiciel, il arrive qu'on aurait à déplacer
des classes et des interfaces dans un diagramme. Ainsi, il peut arriver
que deux interfaces soit contiguës, alors on ne sait plus exactement
à quel interface appartienne les méthodes flottantes:
Classe implémentant une interface avec Rational Rose |
Deux interfaces étendant (extending) deux autres interfaces. |
Il est à noter qu'UML interdit que des interfaces contiennent
des champs (voir encadré plus bas); encore une fois, cette contrainte
des auteurs de UML tient plus à leur méconnaissance du modèle
objet qu'à une contrainte réelement nécessaire.
Heureusement, les programmeurs d'outils de génie logiciel comme
Ratinal Rose n'ont pas tenu compte de cette absurdité et permettent
aux interfaces de contenir des champs; ceux-ci sont nécessairement
des champs de classes (les interfaces étant ininstantiables) dont
la valeur est fixe (ou finale).
| "Interfaces may not have Attributes, Associations, or Methods."
MOG-UML 1.3 March 2000, (Chapter 2 (UML Semantics), 2.5 (Core), Section
2.5.2.22 Interface)
''An Interface can only contain Operations.''
Un interface Java peut avoir des attributs (champs) et des associations (champs typés par une classe du même modèle), à la condition qu'elles soient statiques et finales. Un interface ne peut pas contenir de méthodes (il s'agit ici de méthodes avec le sens que donne UML : une définition d'opération, c'est-à-dire une méthode concrète en Java). |
Une approche qui serait appropriée pour représenter des
interfaces seraient similaire aux classes abstraites, elles pourraient
être représentés à l'aide non pas d'un triangle
imbriquée, comme pour les classes abstraites, mais avec deux triangles
imbriquées, comme à la figure 4:
Figure 4: interface avec la nouvelle approche |
Cette façon de faire est fort bien appropriée au modèle objet: en effet, un interface, c'est un peu un classe ''qui est un peu plus abstraite'' qu'une classe abstraite (puisque qu'un interface ne peut contenir de méthode concrète, au contraire d'une classe abstraite). Nous conservons les trois compartiments des classes: le compartiment du nom du classifier (classe ou interface), le compartiment des champs (eh oui, un interface peut contenir des champs, à condition qu'ils soient des constantes), et enfin le compartiment des méthodes, toutes abstraites évidemment. Un interface peut être implémenté (c'est-à-dire recevoir un lien d'héritage), ce que montre bien le triangle, qui sert de réceptacle au lien d'héritage. Enfin, la finalité d'une classe étant représentée par un triangle inversée, on ne peut représenter un interface de façon finale : cette contrainte correspond parfaitement au modèle objet. En effet, un interface ne peut être final, pusiqu'il sert à l'implémentation de classes.
Toutes les méthodes définies dans un interface étant implicitement abstraites, la représentation de celles-ci est superflue dans un interface. Et comme les méthodes privées n'ont pas à être affichées avec la nouvelle approche, l'outil de génie logiciel n'a pas à valider, comme en UML traditionnel, qu'une méthode ne peut être privée (préfixée du signe '-') dans un interface.
Note : [3] All Features defined in an Interface are public.
self.allFeatures->forAll ( f | f.visibility = #public )
UML V1.3 2.5.3 (Well-Formedness Rules)
B8- Un bout nommé t1[]: genere un Vector de t1 dans T2, et des methodes addT1() et removeT1()
B9- Un bout nommé t1[8]: genere un tableau de 8
elements, et
des methodes addT1() et removeT1()
B10- lien anonyme: 1 pour t1, [] pour t2[]: generation
des
variables a partir des noms en minuscules
de classes (ici
t1 et t2). Un seul lien anonyme
par classe
B11- t2? : can be null, valeur accepte par le setter
B12- constructeur: lien obligatoires, valeurs par defaut
Une des plus grandes failles de UML est de ne pas supporter le concept de constructeur, pourtant central dans le modèle objet, de façon adéquate. En effet, pour UML, un constructeur n'est qu'une sorte de méthode, apparaissant dans le troisième compartiment d'une classe, avec toutes les autres méthodes. Elle peut être notée avec le stéréotype <<Constructor>>, quoique ceci n'est pas vraiment nécessaire, un constructeur ayant toujours le même nom que la classe à laquelle elle appartient.
Pourtant, un constructeur diffère grandement d'une méthode. Un constructeur n'a pas de valeur de retour, et la plupart des modifiants de méthodes ne s'appliquent pas aux constructeurs: un constructeur ne peut être abstrait, final, statique, natif ou synchronized. Ceci n'est pas un caprice d'un langage de programmation donnée, ces contraintes sont tout simplement inhérant au modèle objet.
Pour commencer, précisons que dans le modèle objet, un constructeur n'est pas un membre d'un classe, c'est-à-dire qu'il n'est pas soumis au mécanisme d'héritage. Un champ défini dans une superclasse est hérité dans les sous-classes (bien qu'il peut ne pas être visible s'il a été déclaré private). Une méthode définie dans une superclasse est héritée dans les sous-classes, elle peut donc être overridden. Un constructeur est un concept totalement différent, c'est un bout de code à exécuter à l'instantation d'un objet de la classe. Les constructeurs n'étant pas des membres (des features selon le vocabulaire UML), ils ne sont pas hérités. En fait, une méthode a plus en commun avec un champ qu'avec un constructeur. Modéliser un constructeur comme une méthode laisse croire que les créateurs de UML avaient une piètre connaissance du modèle objet.
Un constructeur n'étant pas hérité, donc ne pouvant être overridden, donc appliquer les concepts d'abstraction et de finalité aux constructeurs est un non-sens. Un constructeur étant utilisé à l'instanstation d'un objet, le concept de staticité (''un constructeur de classe'') est totalement impensable.
Certains langages de programmation permettent d'associer des modifiants de paréllélisme aux méthodes. Java permet de déclarer une méthode synchronized. Une telle méthode ne peut être exécutée en même temps sur le même objet dans deux fils d'exécution (thread). UML définit également ses propres modifiants, il s'agit de concurrent, guarded et synchronized. On voit immédiatement qu'appliquer synchronized sur un constructeur est un illogisme: puisqu'un constructeur crée un nouvel objet à chaque appel, il est évident que le code ne peut être exécuté par deux fils d'éxécution sur le même objet. Pourtant, parce qu'un constructeur est une méthode, UML permet qu'un constructeur soit synchronized.
Voici un exemple de construction d'un objet du type LineNumberReader; cette classe, héritant de Reader, cette classe permet les diverses méthodes read(); ce plus, cette classe ajoute des méthodes permettant d'accéder directement à une ligne donnée. Le fichier devant être lu s'appelle filename. Un tampon (buffer) est utilisé pour une questioin de performance. De plus, le fichier pouvant contenir des caractères avec diacritiques (é, ç, â, etc.), un filtre diacritique est utilisé pour transformer les caractères diacritiques en caractères ASCII standard (é, è, ê doivent être transformé en 'e'; 'ç' en 'c', etc.).
Un programmeur utilise l'instruction suivante:
LineNumberReader reader = new LineNumberReader(
new DiacriticalCharacterFilterReader(
new InputStreamReader(
new BufferedInputStream(
new FileInputStream(
new File(filename))))));
Question: Comment un programmeur en-est arrivé à ce résultat?
Et est-ce que cette construction est optimale? Pour cela, nous avons
besoin de savoir l'arbre des héritages entre les classes d'I/O;
de même que la liste des constructeurs pour chaque classe.
Malheurement, seule l'héritage entre classes est modélisé
en UML:
![]() |
Ajoutons au diagramme des liens de constructions entre classes.
Un lien de construction sera modélisé comme une association
entre deux classes, mais avec les parenthèses près d'une
classe construteure; la classe constructeure possède un constructeur
ayant la classe associée comme paramètre du constructeur.
Pas besoin (comme UML) d'inscrire le nom d'un constructeur, celui-ci ayant
toujours le même nom que la classe à laquelle elle appartient
:
![]() |
On voit immédiatement que le programmeur aurait pu optimiser son code: au lieu de contruire un objet FileInputStream en appellant le constructeur ayant le paramètre File, codé comme suit:
LineNumberReader reader = new LineNumberReader(
new DiacriticalFilterReader(
new InputStreamReader(
new BufferedInputStream(
new
FileInputStream(
new File(filename))))));
Il aurait pu appeller directement le constructeur ayant le paramètre
String:
![]() |
Voici la construction de reader modifiée:
| LineNumberReader reader =
new LineNumberReader( new DiacriticalFilterReader( new InputStreamReader( new BufferedInputStream( new FileInputStream( new File(filename)))))); |
|
LineNumberReader reader =
new LineNumberReader( new DiacriticalFilterReader( new InputStreamReader( new BufferedInputStream( new FileInputStream( filename))))); |
La construction initiale est représenté par 12 étapes,
tel qu'illustré par le diagramme suivant:
![]() |
Pour connaître la construction optimale, il s'agit de trouver
le plus court chemin entre LineNumberReader et String en
suivant la règle suivante: un objet d'un classe peut être
construit en appeller le constructeur ayant comme paramètre la classe
associée, ou n'importe quelle de ses sous-classes:
![]() |
Ainsi, la construction peut être simplié comme suit:
| LineNumberReader reader =
new LineNumberReader( new DiacriticalFilterReader( new InputStreamReader( new BufferedInputStream( new FileInputStream( new File(filename)))))); |
|
LineNumberReader reader =
new LineNumberReader( new BufferedReader( new DiacriticalFilterReader( new FileReader(filename)))); |
Autre exemple: PrintStream()
plusieurs constructeurs d'arités differentes
Simplification du diagramme
Note : Les constructeurs privés ne sont pas affichés. Les constructeurs protégés sont représentés par #(). Lorsqu'une classe est abstraite, les constructeurs ne peuvent pas être publics (puisqu'ils ne servent qu'à être appellés par les sous-classes)..
B13 - Liens de composition
Une composition UML amènent un certain nombre de contraintes au niveau de la multiplicité. Malheureusement, ces contraintes sont difficilement perceptibles dans le diagramming de UML. Avec le formalisme actuel, on peut avoir un modèle parfaitement invalide sous ces yeux et jamais ne s'en rendre compte. Avec la nouvelle approche proposée, les erreurs sautent aux yeux.
Dans une composition UML, la composante ne peut exister en l'absence de son composite (contrainte supplémentaire par rapport à l'agrégation). Le composé tout comme l'agrégé ne peuvent avoir plus d'un composite ou agrégat. De plus, un composé doit avoir un et un seul composite, alors que l'agrégé peut exister meme sans agrégat. Par exemple, HumanBody est le composite de Limb, Limb ne peut exister en l'absence de HumanBody. Par contre, Animal est aussi un composite de Limb. Limb a donc deux classes-composuites (HumanBody et Animal), mais un objet Limb ne avoir qu'un seul composite à la fois (en effet, comment un meme membre pourrait appartenir a la fois a un humain et a un animal?). Pourtant, UML peut sans problemer modeliser qu'un meme membre peut appartenir a la fois a un humain et a un animal; pis, une tel absurdité sera difficilement détectable dans un diagramme UML à moins d'etre tres attentif.
Dans le diagramme suivant, l'élément TaggedValue a deux composites: ModelElememnt et Stereotype. Il n'y a rien qui empëche cela dans UML. Par contre, il y a une erreur du côté de la multiplicité (l'erreur, si elle n'était pas indiquée en rouge, ne serait pas facile à détecter) :

En effet, si TaggedValue appartient à un et un seul ModelElement et à un et un seul Stereotype en meme temps, de qui est-il le composé? Le diagramme est donc erroné, meme si sa représentation graphique est permise par UML, ce qui contrevient ainsi au principe no 3:
Principe 3 (principe de contrainte) : Autant que possible, tout concept impossible syntaxiquement devrait être impossible à representer graphiquement; sinon c'est à l'outil de génie logiciel d'empêcher une telle représentation.
En placant les multiplicités, ainsi que l'icone de composition, du cote du composé, on se rend plus facilement compte de l'erreur, et un peut ainsi corriger les multiplicité : le TaggedValue peut appartenir à zéro ou un ModelElement, et à zéro ou un Stereotype (il doit avoir un et un seul composite, mais celui-ci peut etre ou bien un ModelElement ou bien un Stereotype).

Une facon encore meilleur de representer le concept de composition est de ne permettre qu'un seul lien de composition par concept (un seul lozange noir), mais ce lozange peut etre rattaché a plus d'un concept (le TaggedValue a un et un seul composite, mais ce composite peut etre n'importe laquelle des classes reliés a ce composé). Le multiplicité, qui est toujours de 1, est alors superfloue, ce qui permet de l'enlever du diagramme, alléchant ainsi l'aspect graphique du diagramme.

B14- valeur de retour: lien representé par ---()
B15- les liens peuvent etre composantes de clefs, prefixé
par
<1>, <2>, etc.
B16- regles de destruction:
lien one-to-many : enfant
d'abord, parent ensuite
lien many-to-many: arbitraire
mais consistent (ex: ordre
alphabetique
des classes)
B17 - modelisation des exceptions
[3] All Features defined in an Interface are public.
self.allFeatures->forAll ( f | f.visibility = #public
)
UML V1.3 2.5.3 (Well-Formedness Rules) en Java?
3-
Each attribute declared in a class has a visibility and a type. The
visibility defines if the
attribute is publicly available to any class, if it is only available
inside the class and its
subclasses (protected), or if it can only be used inside the class
(private).
UML V1.3 (2.5.4 Semantics)