Projet : étape 5.1
Impact différencié des nutriments

Buts: Faire en sorte que telle ou telle sorte de nutriments n'ait pas le même effet sur tel ou tel type bactérie.

La cible permettant de compiler et lancer cette partie est application. Notez que Ctrl-F permet de faire une recherche dans l'éditeur de QtCreator (pour chercher où se trouve application par exemple).

Les nutriments ont joué jusqu'ici le même rôle par rapport à tous les types de bactéries. Si une bactérie, quelle qu'elle soit, rencontre une source de nutriments, elle en consomme (la quantité maximale possible pour elle si tant est qu'une telle quantité soit disponible).

Que faire maintenant si l'on souhaite modéliser le fait que les nutriments jaunes soient toujours consommés ainsi, mais qu'il n'en soit pas de même pour les nutriments bleus. Par exemple :

On souhaite donc faire en sorte que les nutriments ne soient pas consommés de la même manière selon le type de bactéries.

Ce qui va changer à chaque fois, c'est le calcul de la quantité consommée.

L'idée la plus évidente serait (notez le conditionnel) d'introduire une méthode, par exemple eat, dans Bacterium qui se chargerait de gérer la consommation de nutriments :

void eat(Nutrient& nutriment) {
  Quantity eaten;
  // si nutriment est un NutrientA alors calculer eaten d'une certaine façon
  // et s'il est de type NutrientB alors le calculer d'une autre façon.
  // la bactérie consomme alors la quantité eaten (son énergie est "augmentée" d'autant)
  // + éventuelles autres instructions liées à la consommation de nutriments
}

hum... nous faisons là de vilains tests de type.

[Question Q5.1] Pourquoi tester le type des objets à l'exécution est-il potentiellement nocif ? Répondez à cette question, en justifiant vos choix, dans votre fichier REPONSES.

Le problème est que les méthodes en C++ ne sont pas polymorphiques sur les arguments, mais uniquement sur this (le concept de double dispatch à proprement parlé n'est pas offert par le C++). Il est possible de contourner ce problème en ayant recours à un schéma de conception particulier. Techniquement, nous allons «mimer le double dispatch» au moyen de la surcharge (overloading) et la redéfinition de méthodes (overriding). Cette technique est ce que l'on appelle un «patron de conception» (il en existe plusieurs et celui que nous mettons en place s'apparente à celui dit du «visitor pattern»).

L'idée est que l'on peut "retourner" l'argument en écrivant:

void eat(Nutrient& nutriment) { //on ne peut pas être polymorphique directement sur le paramètre
  Quantity eaten(nutriment.eatenBy(*this)); //on s'arrange pour l'être en invoquant une méthode  polymorphique dessus
  
  // + éventuelles autres instructions liées à la consommation de nutriments
}
eatenBy serait une méthode polymorphique sur les nutriments, surchargée pour chaque type de bactérie. Elle serait redéfinie dans les sous-classes de Nutrient et calculerait pour chaque type de bactérie la quantité cédée par le nutriment en question.
"Retourner l'argument" revient simplement à changer de point de vue: la quantité consommée par la bactérie est la quantité que le nutriment cède à cette bactérie. Ceci permet d'appliquer le polymorphisme du point de vue du nutriment et d'éviter ainsi des tests de type.
On définirait donc dans Nutrient des méthodes virtuelles pures, surchargées par rapport au type de la bactérie impliquée :
virtual Quantity eatenBy(Bacterium& bact) = 0;
virtual Quantity eatenBy(MonotrichousBacterium& bact) = 0;
virtual Quantity eatenBy(PilusMediatedBacterium& bact) = 0;
// et pareil pour toute les autres sous-classes de bactéries
dans NutrientA la méthode eatenBy(MonotrichousBacterium&) serait redéfinie concrètement par quelque chose comme :
Quantity NutrientA::eatenBy(MonotrichousBacterium& bacterium)
{
    return takeQuantity(bacterium.getMaxEatableQuantity()); // à adapter à votre code
}
et dans NutrientB comme:
Quantity NutrientB::eatenBy(MonotrichousBacterium& bacterium)
{
    //... retrouver le facteur de résistance à la consommation (factor)
    return takeQuantity(bacterium.getMaxEatableQuantity() / factor);
}

[Question Q5.2] Pourquoi doit-on prévoir une méthode virtual Quantity eatenBy(Bacterium& bact) const = 0; avec comme argument une Bacterium quelconque (alors qu'à priori seule nous intéresse la définition de eatenBy pour des sous-classes de bactéries) ? Répondez à cette question dans votre fichier REPONSES.

Cette méthode est nécessaire, mais comment la coder concrètement dans les sous-classes NutrientA et NutrientB.

Elle doit retourner la quantité du nutriment consommable par une bactérie, et l'on se retrouve donc à nouveau dans la situation où un argument devrait être traité de façon polymorphique!!

La méthode Quantity eatenBy(Bacterium&) de NutrientA et NutrientB sera donc simplement codée comme:

bact.eatableQuantity(*this);

Vous définirez dans la super-classe Bacterium les méthodes:

virtual Quantity eatableQuantity(NutrientA& nutriment) = 0;
virtual Quantity eatableQuantity(NutrientB& nutriment) = 0;
qui seront redéfinies dans les sous-classes selon l'exemple suivant (cas des bactéries à flagelle unique) :
Quantity MonotrichousBacterium::eatableQuantity(NutrientA& nutriment)
{
    return nutriment.eatenBy(*this);
}
Selon le point de vue, on a donc tantôt besoin de calculer la quantité cédée par la source de nutriments à une bactérie (eatenBy) ou la quantité consommée par la bactérie d'un nutriment donné (eatableQuantity).

Et pour résumer: la méthode void Bacterium::eat fait appel à la méthode eatenBy(Bacterium&) des nutriments. Cet appel permet de "brancher" sur la méthode eatableQuantity de la bonne sous-classe de bactérie (polymorphisme). Cette dernière fait alors appel à la méthode eatenBy du bon type de nutriment sur le bon type de bactérie... ouf on y arrive !!

Il vous est donc demandé de coder de façon appropriée les méthodes eatenBy (dans la hiérarchie de Nutrient) et eatableQuantity (dans la hiérarchie de Bacterium) en vous inspirant des exemples donnés ci-dessus.

Pour le codage des méthodes eatenBy, vous considérerez que:

On simule de cette manière que les nutriments bleus sont plus nutritifs pour les bactéries à grappin, toxiques pour les bactéries avec déplacement groupé et plus difficiles à consommer pour les bactéries simples. Les nutriments jaunes restent consommables comme ils l'étaient auparavant, mais toute la logistique est en place pour changer cet état de fait si on le souhaite.
Attention ici aux situations de dépendances circulaires : la classe Bacterium a besoin de Nutrient laquelle a à son tour besoin du type Bacterium. Dans les fichiers .hpp, de ces classes, contentez-vous donc de prédéclarer les classes utiles. Ceci se fait comme ceci (exemple de la classe Bacterium):
class Nutrient;
class NutrientA;
class NutrientB;
à placer avant la déclaration de la classe Bacterium dans Bacterium.hpp. Il faut alors inclure les .hpp de ces classes dans le fichier Bacterium.cpp.

Test 17 : «Double dispatch»

Pour tester cette partie vous pouvez utiliser l'application finale fournie src/Tests/GraphicalTests/FinalApplication.cpp qui est associée à la cible application dans le fichier CMakeLists.txt fourni pour cette étape.

Vous pouvez y lancer la création des diverses bactéries codées précédemment au moyen des touches 'M', 'P', les touches '1', '2' ou '3' (pour les bactéries avec déplacement groupé).

Ralentissez la cadence de la simulation et créez différents types de bactéries sur différents types de nutriments. Vous devriez pouvoir observer les impacts différenciés des nutriments sur les bactéries.

Par exemple, dans la vidéo ci-dessous on peut voir que la bactérie à flagelle unique mange les nutriments bleus avec plus de difficulté que les nutriments jaunes et que les bactéries avec déplacement groupé se font rapidement «tuer» par les nutriments bleus :


Retour à l'énoncé du projet (partie 5) Module suivant (partie 5.2)