Relation entre classe et objet
On a vu comment créer et manipuler des objets au chapitre 4 et on sait maintenant définir nos propres objets en écrivant des classes. Ce qu'on va voir maintenant, c'est comment appliquer les principes de la programmation orientée objets. Dans cette section, on va voir les différentes relations qu'il est possible de définir entre différentes classes, en commençant avec la composition et l'agrégation, deux relations qui vont créer une dépendances entre classes, et donc entre les objets créés à partir de ces classes.
Composition
Supposons qu'on souhaite définir une nouvelle classe PairOfDice
qui représente une paire de dés. Les deux dés de la paire doivent posséder le même nombre de faces et on veut pouvoir lancer les deux dés en même temps et regarder leurs faces visibles respectives.
Voici la définition d'une première version de la classe :
Voici comment créer une paire de dés à six faces et afficher la valeur des deux faces visibles sur la sortie standard :
L'exécution de ce programme pourrait par exemple afficher ceci :
5,2
Cette classe est tout à fait correcte et fait bien ce qu'il faut, mais elle n'est pas très élégante. En effet, on n'exploite pas toutes les possibilités de la programmation orientée objets. Un signe qui indique une mauvaise structuration est la répétition de code, et l'utilisation de code qui a déjà été défini dans une autre classe. Ce qui serait judicieux, ce serait de pouvoir réutiliser la classe Die
pour construire la classe PairOfDice
.
Et c'est ce qu'on va faire. On va utiliser des objets Die
pour définir les objets PairOfDice
. Une paire de dés est constituée de deux dés et on va donc utiliser deux variables d'instance de type Die
dans la classe PairOfDice
. Voici la nouvelle version de la classe :
On utilise donc un objet pour en définir un autre. Toutes les instances de la classe PairOfDice
sont composées de deux instances de la classe Die
. Il s'agit de la première relation entre objets que l'on découvre : la relation de composition.
Celle-ci s'appelle également la relation has-a. Il s'agit d'une relation qui lie deux classes. On dit qu'une classe $A$ est composée à partir d'une classe $B$, ce qui signifie qu'une instance de la classe $A$ aura une variable d'instance de type $B$.
La figure 17 montre les deux classes Die
et PairOfDice
ainsi que la relation qui les lie. Celle-ci est représentée par une flèche se terminant par un diamant et va de l'objet principal vers celui dont il est composé. On peut également indiquer à l'aide d'un entier le nombre de composants utilisés. La lecture de ce schéma donne : « Chaque instance de classe PairOfDice
est composée de deux instances de la classe Die
».
Voyons maintenant ce qui se passe en mémoire. La figure 18 montre tout ce qui se trouve en mémoire après exécution de l'instruction PairOfDice pair = new PairOfDice (6);
. Il s'agit bien entendu d'une possibilité puisque les valeurs des faces visibles sont choisies au hasard.
On voit clairement sur la figure qu'il y a trois objets qui ont été créés en tout. En effet, il y a tout d'abord l'objet PairOfDice
et ses trois variables d'instance. Ensuite, on retrouve les deux objets Die
qui ont été créés dans le constructeur de la classe PairOfDice
. La relation de composition entre les deux classes s'est traduite par des variables d'instance de type objet contenant des références vers des objets Die
.
Grâce à cette relation, on va pouvoir décomposer la définition d'un objet complexe en plusieurs définitions d'objets plus élémentaires et en les composant entres eux. Il s'agit bel et bien d'un exemple d'organisation de code exploitant la richesse de l'orienté objets.
Supposons qu'on souhaite écrire un programme qui manipule des voitures, par exemple pour une application qui permette à des clients de construire la voiture de leur rêve via une site Internet en choisissant le type de roues, de volant et de sièges. On définirait par exemple les classes Car
, Wheel
, SteeringWheel
et Seat
. Une voiture étant composée de roues, de volant et de sièges, on aura les relations Car
est composée à partir de Wheel
, SteeringWheel
et Seat
.
Un autre exemple
Prenons un autre exemple. Soit une classe Coordinate
qui représente les coordonnées d'un point dans le plan, c'est-à-dire un couple de nombres $(x, y)$. On va donc utiliser deux variables d'instance de type double
et ajouter des méthodes accesseurs pour ces nombres :
Construisons maintenant une classe pour représenter un rectangle horizontal. Un rectangle est caractérisé par une longueur (la longueur du côté horizontal), une hauteur (la longueur du côté vertical) et une position dans le plan (coordonnée du coin inférieur gauche). On va tout naturellement utiliser la classe Coordinate
qu'on vient d'écrire pour représenter la coordonnée du rectangle grâce à une relation de composition comme le montre la figure 19.
Le constructeur de la classe Rectangle
prend quatre paramètres qui correspondent respectivement aux coordonnées $x$ et $y$ du coin inférieur gauche, à la largeur et à la hauteur du rectangle :
Agrégation
Dans la relation de composition que l'on vient de voir, le lien entre les classes liées entre elles est très fort. Cela signifie que lorsque l'instance de la classe composite disparait de la mémoire, les instances composées disparaissent également. Dans le premier exemple, lorsque la paire de dés sera détruire, les deux dés la composant seront également détruits. Il en est de même pour la coordonnée du rectangle.
Une relation plus générale que la composition est l'agrégation. Il s'agit donc d'une forme généralisée de la composition, mais sans l'appartenance. Des objets vont pouvoir être agrégés, mais sans devenir fortement liés à un autre objet. Si on veut écrire un programme pour gérer les communes d'un pays, on pourrait avoir une classe City
pour représenter une ville, celle-ci étant dirigée par un bourgmestre instance d'une classe Citizen
représentant un citoyen. La figure 20 montre cette relation d'agrégation, représentée par un diamant plein. Une différence à noter est le chiffre $0..1$ signifiant qu'un objet City
peut stocker zéro ou une référence vers un objet Citizen
.
Voici le code de la classe City
, qui contient donc une variable d'instance de type Citizen
et une méthode mutateur permettant de la modifier :
La différence entre composition et agrégation est donc que dans le deuxième cas, les deux objets sont moins liés, puisque la la suppression d'une instance de la classe City
n'entrainera pas la suppression des objets agrégés.
Relation uses
Enfin, une autre relation possible entre deux classes est la relation uses. Cette relation est très générale et indique simplement qu'une classe utilise une autre classe. Elle peut simplement créer une instance d'une autre classe ou avoir une méthode qui renvoie un objet du type d'une autre classe ou encore avoir une méthode qui prend en paramètre un objet du type d'une autre classe. Les relations de composition et d'agrégation sont évidemment des cas particuliers de la relation uses.
On peut par exemple ajouter une méthode contains
à la classe Rectangle
, pour tester si une coordonnée particulière se trouve dans la surface du rectangle ou non. Cette méthode prend en paramètre un objet de type Coordinate
et on a donc la relation « Rectangle
uses Coordinate
» :
Contrairement à la composition et l'agrégation qui se traduisent par une relation entre les objets, la relation uses peut ne s'appliquer qu'entre deux classes. L'exemple suivant montre une classe Distance
qui permet de calculer la distance entre deux coordonnées qui sont lues depuis l'entrée standard :
La relation uses est représentée par une flèche normale qui part de la classe qui utilise vers la classe utilisée. La figure 21 montre les différentes relations uses de cet exemple. La classe Distance
utilise les classes Scanner
, Coordinate
et Math
. On pourrait même ajouter la classe PrintStream
puisqu'on utilise System.out
.
Interaction entre objets de la même classe
Enfin, on va maintenant s'intéresser à des interactions entre objets de la même classe. Voici par exemple une méthode contains
qui se trouve dans la classe Rectangle
et qui est une version surchargée de celle qu'on a vue dans la section précédente.
Cette version permet de tester si un rectangle contient un autre rectangle. La méthode permet donc de vérifier si le rectangle représenté par l'objet cible contient le rectangle représenté par l'objet passé en paramètre :
On est donc dans une situation où une méthode de la classe Rectangle
prend en paramètre un objet de type Rectangle
. Une telle situation est tout à fait légale et très utile. Pour pouvoir faire le test, on va devoir accéder à l'état du rectangle référencé par la variable rect
, or on n'a pas défini d'accesseur dans cette classe. Mais on va en fait pouvoir accéder directement aux variables d'instances même si elles sont private
, rappelez-vous la règle d'accès aux variables d'instance.
On accède par exemple directement à la largeur du rectangle avec l'expression suivante :
On peut donc utiliser une classe qu'on est en train de définir dans la définition même de cette dernière. L'exemple qu'on vient de voir utilisait la classe comme paramètre. On peut également créer et renvoyer des instances de cette classe. La méthode suivante permet de faire une copie du rectangle tout en lui appliquant une mise à l'échelle du facteur spécifié :
Voyons maintenant comment on peut utiliser ces différentes méthodes :
L'exécution du programme affiche sur la sortie standard :
true false true
On vient de voir la seconde manière de créer un objet, à savoir en appelant une méthode; la première consistant à directement le créer avec l'opérateur new
. On vient de faire une indirection, c'est-à-dire appeler une méthode qui va s'occuper de la création de l'objet puis renvoyer une référence vers ce dernier. La figure 22 montre les trois objets créés en mémoire par ce programme.