Composition et agrégation
Composition et agrégation permettent de lier des classes entres elles et plus principalement lorsqu'une classe admet comme attribut des objets de l'autre classe.
Ce qui les distingue :
Définition :
- agrégation : quand les objets utilisés sont créés en dehors de la classe,
- composition : quand les objets utilisés sont créés dans le constructeur de la classe qui les utilise.
Il est important de comprendre que si des objets n'ont pas été crées dans la classe qui l'utilise, ils peuvent être connus par d'autres méthodes du programme et donc être modifiées par celles-ci.
Les exemples de composition et d'agrégation de la vraie vie sont souvent un peu bizarres. Mais par exemple :
- Un livre est composé de pages : pour créer le livre on a créé les pages : c'est une composition
- Les télécommandes ont besoin de piles pour fonctionner, mais on peut les remplacer : c'est une agrégation.
Schémas uml
Lorsque l'on utilise la composition ou l'agrégation de nos classes dans des schéma uml, on liera la classe composé (resp. agrégée) à la classe l'utilisant par une flèche. Cette flèche sera différente pour une composition ou une agrégation :
Exemple du panier de fruit
Prenons un autre exemple, le panier de fruits. Je voudrais modéliser un panier de fruits. Il doit avoir les propriétés suivantes :
- il doit être vide initialement
- je dois pouvoir ajouter des fruits dans le panier
- je dois pouvoir montrer les fruits que j'ai dans le panier
- je dois pouvoir reprendre un fruit du panier
Modélisation uml
Comme à notre habitude commençons par créer une boîte uml presque vide :
Pour que l'on puisse faire ces différentes méthodes, il faut que Panier
puisse stocker ses fruit. On lui ajoute donc un attribut qui sera une liste.
Composition et agrégation
- Le panier est composé d'un stock : il est créé avec le panier
- le stock agrège des fruits : ils sont ajoutés par une méthode dans l'objet.
Code python
Ceci s'implémente aisément en python :
class Panier:
def __init__(self):
self.stock = []
def ajoute(self, fruit):
self.stock.append(fruit)
def montre_panier(self):
return self.stock
def supprime(self, fruit):
self.stock.remove(fruit)
On peut alors utiliser notre classe, par exemple :
panier = Panier()
print(panier.montre_panier())
panier.ajoute("pomme")
print(panier.montre_panier())
panier.ajoute("pomme")
panier.ajoute("poire")
print(panier.montre_panier())
panier.supprime("pomme")
print(panier.montre_panier())
Attention
Si une classe est composée d'autres objets, ces parties peuvent être modifiées en dehors de la classe, même pour une composition.
Dans notre exemple, une méthode retourne un objet qui est un attribut. Une fois qu'un objet a été donné au monde extérieur on ne contrôle plus son état et il peut être utilisé a priori par n'importe quoi d'autre dans le programme.
Regardez le code suivant :
panier = Panier()
print(panier.montre_panier())
panier.ajoute("pomme")
copie = panier.montre_panier()
copie.append("champignon")
print(panier.montre_panier())
On a ajouté un champignon à notre panier sans que l'objet panier ne le sache !
Ce n'est pas forcément un problème, sauf si on vérifiait que ce qu'on ajoute dans le panier doit être un fruit.
Pour que tout se passe comme prévu, il faut donc s'assurer que pour les attributs , soit :
- on s'en fiche qu'ils changent
- on refait un nouvel objet à nous pour s'assurer qu'il ne changera pas
- les objets que l'on retourne et qui sont des attributs sont non modifiables (des entiers, réels, chaînes de caractères, tuples, etc)
1ère solution
La 1ère solution est ce qu'on a pour l'instant.
2nde solution
La seconde solution reviendrait à donner un nouveau panier à chaque fois en modifiant la méthode montre_panier()
:
class Panier:
# ...
def montre_panier(self):
return list(self.stock)
#...
3ère solution
Enfin, la dernière solution reviendrait à ne pas pouvoir modifier l'attribut et à le recréer à chaque modification, dans les méthodes ajoute()
et supprime()
. On utilise un tuple qui est une liste sans possibilité de modification. Le code suivant crée un nouveau tuple en utilisant l'opération +
des tuples qui crée un nouvel objet. :
class Panier:
def __init__(self):
self.stock = tuple()
# ...
def ajoute(self, fruit):
self.stock = self.stock + (fruit,)
def supprime(self, fruit):
stock_temporaire = list(self.stock)
stock_temporaire.remove(fruit)
self.stock = tuple(stock_temporaire)
#...
Selon que l'on aura beaucoup d'ajouts ou beaucoup de visualisation du panier, on choisira l'une ou l'autre solution. Mais si on a pas d'idée, on préférera toujours la 3ème solution qui est la plus robuste.
Le fait d'avoir des objets qui ne se modifient pas est appelé value object.
Ces objets possèdent des valeurs et des méthodes pour y accéder mais que l'on ne peut pas modifier. La seule façon de changer de valeur c'est de recréer de nouveaux objets. Pour que l'utilisation de ce genre d'objets soit fluide, on fait en sorte qu'ils puissent supporter l'opération ==
.
Vous avez utilisé des value objects bien souvent en python comme : les entiers, les réels ou encore les chaines de caractères. Enfin de nombreux objets modifiables en python ont leur contrepartie non modifiable comme les tuple
qui sont des listes non modifiables ou encore les frozenset
sont des ensembles non modifiables.
Une bonne façon de programmer est d'utiliser par défaut uniquement des objets non modifiables et que si le besoin s'en fait sentir de les rendre modifiables.
Tests des objets
Lorsque l'on crée ses propres objets, il est important de tester leurs fonctionnalités. On procède alors ainsi :
- chaque méthode doit être testée
- chaque test doit être indépendant
Dans la mesure du possible, on ne teste pas la valeur des attributs. On utilise des méthodes publique pour tester. En effet les attributs montrent l'implémentation de la classe et pas son usage.
Exemple
Par exemple, pour notre panier. Fichier test_panier.py
:
from panier import Panier
def test_init():
panier = Panier()
assert panier is not None
def test_montre_panier_vide():
panier = Panier()
assert panier.montre_panier() == tuple()
def test_ajoute():
panier = Panier()
panier.ajoute("pomme")
assert panier.montre_panier() == ("pomme",)
def test_supprime_dans_panier():
panier = Panier()
panier.ajoute("pomme")
panier.supprime("pomme")
assert panier.montre_panier() == tuple()
- pour l'initialisation, on vérifie juste que notre objet existe. Comme il n'a pas de paramètre, on ne peut pas tester grand chose d'autre
- On crée pour chaque test un nouvel objet, pour être sur que les tests n'interfèrent pas les uns avec les autres
- Chaque test doit permettre d'utiliser la méthode testée comme elle doit être utilisée dans le code
Code final
Vous devez avoir 3 fichiers :
Code de la classe
Fichier : panier.py
class Panier:
def __init__(self):
self.stock = []
def ajoute(self, fruit):
self.stock.append(fruit)
def montre_panier(self):
return tuple(self.stock)
def supprime(self, fruit):
self.stock.remove(fruit)
Programme principal
Fichier main.py
:
from panier import Panier
panier = Panier()
print(panier.montre_panier())
panier.ajoute("pomme")
print(panier.montre_panier())
panier.ajoute("pomme")
panier.ajoute("poire")
print(panier.montre_panier())
panier.supprime("pomme")
print(panier.montre_panier())
Tests
Fichier test_panier.py
:
from panier import Panier
def test_init():
panier = Panier()
assert panier is not None
def test_montre_panier_vide():
panier = Panier()
assert panier.montre_panier() == tuple()
def test_ajoute():
panier = Panier()
panier.ajoute("pomme")
assert panier.montre_panier() == ("pomme",)
def test_supprime_dans_panier():
panier = Panier()
panier.ajoute("pomme")
panier.supprime("pomme")
assert panier.montre_panier() == tuple()