Comme indiqué dans l'introduction de cette série de laboratoires, nous voulons peupler une fenêtre graphique avec des entités d'aspect et de comportement différents.
Pour le moment, pour simplifier, nous n'avons travaillé que sur des carrés.
Comment pouvons nous introduire de nouvelles formes graphiques ?
Faire des copies du type
Entity en le changeant de nom et en réadaptant un petit peu le code pour avoir des cercles, des rectangles, des triangles, etc. pourrait sembler une solution rapide.
Pourtant, elle est certainement la solution la moins facile à écrire proprement et à maintenir.
En effet, en répétant le code plusieurs fois, des erreurs deviennent inévitables.
En outre, ce code ne garantirait en aucun cas que toutes les entités graphiques offrent les mêmes fonctionnalités (en cas d'oubli d'une fonction dans une nouvelle classe, par exemple).
De plus, ajouter une nouvelle forme impliquerait plusieurs modifications dans le code existant, notamment dans la classe
Window qui devrait alors invoquer les fonctions de chaque type de forme graphique pour dessiner, animer et faire réagir au clic.
Ce dont nous aurions besoin c'est d'un mécanisme qui permettrait de spécifier le comportement que tout objet graphique doit garantir et qui permettrait à la classe
Window de traiter tous ces objets de la même façon.
La classe
Window les traiterait comme s'ils étaient tous du même type sans avoir besoin de savoir s'il s'agit d'un cercle, d'un rectangle, ou d'un triangle, etc. et donc d'écrire du code spécifique pour chaque type d'objet graphique qu'elle contient.
En d'autres termes, nous avons besoin pour tous ces objets graphiques d'une abstraction commune qui fixe une fois pour toutes le comportement que tout objet graphique fournit.
Cela tombe bien, la programmation orientée objet nous offre précisément un tel mécanisme qui s'appelle une interface.
Pour rappel, l'interface en POO spécifie le comportement (ensemble de fonctions) que toute classe qui l'implémente doit garantir.
L'interface déclare alors un ensemble de fonctions abstraites, qui ne possèdent pas d'implémentation, ou de fonctions concrètes avec une implémentation par défaut.
Tout type implémentant l'interface s'engage à fournir une définition pour chaque fonction abstraite, peut aussi redéfinir les fonctions default ou les hériter telles quelles. Un type implémentant une interface hérite aussi du type de l'interface même.
Par exemple, supposons que nous ayons une interface I et trois classes A, B et C qui l'implémentent.
Toute instance de A sera à la fois de type A et I, tout instance de B sera à la fois de type B et I...
En d'autres termes, toutes les instances de A, B et C sont aussi de type I, ce qui assure qu'elles ont un type abstrait commun et ont toutes le même comportement.
Ce comportement se décline différemment selon les besoins, les classes A, B et C implémenteront à leur façon les fonctions de l’interface, qui sont donc polymorphes.
Ce polymorphisme est un des concepts fondamentaux de la POO.
L'interface est désormais un mécanisme fondamental de la programmation orientée objet et les langages orientés objet fournissent des moyens et des mot clefs pour les déclarer.
Pour notre problème, nous allons nous servir de ce mécanisme et nous allons modifier l’architecture de notre code pour suivre le diagramme de classes suivant :
Le type
Entity ne s'occupera plus directement de son dessin, en revanche, il sera lié par une composition à 1 composant à l'interface
Shape.
Cela veut dire qu'
Entity pourra posséder un objet de n’importe quel type qui implémente
Shape.
Commencez alors par fournir l’interface
Shape. En Rust, une interface s’écrit en déclarant un
trait.
Pour rappel : un trait définit un ensemble de fonctionnalités que différents types (implémentant le trait) possèdent et partagent.
Déclarez le trait
Shape dans le fichier
simu.rs (après la classe
Entity, par exemple).
Ce trait doit déclarer toutes les fonctionnalités que n’importe quel type de forme graphique doit fournir, on parle aussi de comportement partagé (
shared behavior).
En analysant attentivement la classe
Entity dans son état actuel, nous remarquons que savoir se dessiner et fournir le rayon de son cercle englobant sont deux comportements strictement liés à sa forme graphique.
Le trait
Shape doit alors déclarer deux fonctions abstraites :
- bounding_radius() : qui renvoie un réel (le rayon),
- draw(), qui ne renvoie rien et reçoit en paramètre l’entité et l’écran sur lequel la dessiner.
Évidemment, le trait
Shape ne peut pas fournir d’implémentation pour ces deux fonctions, il ne possède aucune information spécifique à une quelconque forme graphique.
L’implémentation de ces fonctions ne pourra être fournie que par les types implémentant le trait.
Le diagramme de classes nous montre déjà le premier type implémentant le trait
Shape que vous devrez fournir : la classe
Rectangle.