Project : step 5.1
Variable impact of nutrients

Disclaimer: the automatically generated English translation is provided only for convenience and it may contain wording flaws. The original French document must be taken as reference!

Goals: To ensure that a particular type of nutrient does not have the same effect on a particular type of bacterium.

The target used to compile and run this section is application. You must uncomment it when the time comes in the CMakeLists.txt. Note that Ctrl-F allows you to search within the QtCreator editor (to find where application is located, for example).

So far, nutrients have played the same role for all types of bacteria. If any bacterium encounters a source of nutrients, it consumes them (the maximum amount possible for it, provided such an amount is available).

What should we do now if we wish to model the fact that yellow nutrients are always consumed in this way, but that the same does not apply to blue nutrients? For example :

We therefore want to ensure that nutrients are not consumed in the same way depending on the type of bacteria.

What will change each time is the calculation of the quantity consumed.

The most obvious idea would be (note the conditional) to introduce a method, for example eat, in Bacterium which would be responsible for managing nutrient consumption :

void eat(Nutrient& nutriment) {
  Quantity eaten;
  //  if nutrient is a NutrientA then calculate eaten in a certain way
  // and if it is of type NutrientB then calculate it in another way.
  // the bacterium then consumes the quantity eaten (its energy is "increased" by that amount)
  // + any other instructions related to nutrient consumption
}

hmm... we’re doing some bad type testing here.

[Question Q5.1] Why is checking the type of objects at runtime potentially harmful ? Answer this question, justifying your choices, in your REPONSES file.

The problem is that methods in C++ are not polymorphic on arguments, but only on this (the concept of double dispatch strictly speaking is not offered by C++). It is possible to work around this problem by using a specific design pattern. Technically, we will «mimic a double dispatch» by means of overloading and overriding. This technique is known as a «design pattern» (there are several of these, and the one we are implementing is similar to the so-called «visitor pattern»).

The idea is that we can "swap" the argument by writting:

void eat(Nutrient& nutriment) { //we cannot be polymorphic directly on the parameter
  Quantity eaten(nutriment.eatenBy(*this)); //we make it so by invoking a polymorphic method on it
  
  // + any other instructions related to nutrient consumption
}
where eatenBy would be a polymorphic method on nutrients, overloaded for each type of bacterium. It would be redefined in the subclasses of Nutrient and would calculate, for each type of bacterium, the quantity yielded by the nutrient in question.
"Swap the argument" simply comes down to changing perspective: the amount consumed by the bacterium is the amount that the nutrient yields to that bacterium. This allows polymorphism to be applied from the nutrient’s perspective, thereby avoiding type checks.
We would therefore define in Nutrient pure virtual methods, overloaded according to the type of bacterium involved :
virtual Quantity eatenBy(Bacterium& bact) = 0;
virtual Quantity eatenBy(MonotrichousBacterium& bact) = 0;
virtual Quantity eatenBy(PilusMediatedBacterium& bact) = 0;
// and similarly for all other subclasses of bacteria
in NutrientA the method eatenBy(MonotrichousBacterium&) would be concretely redefined as something like:
Quantity NutrientA::eatenBy(MonotrichousBacterium& bacterium)
{
    return takeQuantity(bacterium.getMaxEatableQuantity()); // adapt to your code
}
and in NutrientB like:
Quantity NutrientB::eatenBy(MonotrichousBacterium& bacterium)
{
    //... retrieve the consumption resistance factor (factor)
    return takeQuantity(bacterium.getMaxEatableQuantity() / factor);
}

[Question Q5.2] Why do we need to define a method virtual Quantity eatenBy(Bacterium& bact) const = 0; with any Bacterium as an argument (even though, in principle, we are only interested in the definition of eatenBy for subclasses of bacteria)? Answer this question in your REPONSES file.

This method is necessary, but how should it be implemented in the subclasses NutrientA and NutrientB?

It must return the quantity of the nutrient that can be consumed by a bacterium, so we find ourselves once again in a situation where an argument needs to be handled polymorphically!!

The method Quantity eatenBy(Bacterium&) of NutrientA and NutrientB will therefore simply be coded as:

bact.eatableQuantity(*this);

You will define the following methods in the superclass Bacterium:

virtual Quantity eatableQuantity(NutrientA& nutriment) = 0;
virtual Quantity eatableQuantity(NutrientB& nutriment) = 0;
which will be overridden in the subclasses as shown in the following example (case of single-flagellated bacteria):
Quantity MonotrichousBacterium::eatableQuantity(NutrientA& nutriment)
{
    return nutriment.eatenBy(*this);
}
Depending on the perspective, we therefore sometimes need to calculate the quantity transferred from the nutrient source to a bacterium (eatenBy) or the quantity of a given nutrient consumed by the bacterium (eatableQuantity).

To summarise: the method void Bacterium::eat calls the method eatenBy(Bacterium&) of the nutrients. This call allows us to "hook into" the eatableQuantity method of the correct bacterium subclass (polymorphism). This method then calls the eatenBy method of the correct nutrient type on the correct bacterium type... phew, we’re getting there!!

You are therefore asked to implement the methods eatenBy (in the Nutrient hierarchy) and eatableQuantity (in the Bacterium hierarchy) appropriately, drawing inspiration from the examples given above.

When coding the eatenBy methods, you should consider that:

This simulates the fact that blue nutrients are more nutritious for grab-type bacteria, toxic to bacteria with group movement, and more difficult for simple bacteria to consume. Yellow nutrients remain consumable as they were before, but all the necessary mechanisms are in place to change this if desired.
Be careful here with circular dependencies : the class Bacterium requires Nutrient, which in turn requires the type Bacterium. In the .hpp files for these classes, simply pre-declare the relevant classes. This is done as follows (example using the Bacterium class):
class Nutrient;
class NutrientA;
class NutrientB;
to be placed before the declaration of the Bacterium class in Bacterium.hpp. You must then include the .hpp files for these classes in the Bacterium.cpp file.

Test 17 : «Double dispatch»

To test this section, you can use the final application provided in src/Tests/GraphicalTests/FinalApplication.cpp, which is associated with the target application in the CMakeLists.txt file provided for this step.

You can use the keys 'M', 'P', and the keys '1', '2' or '3' (for bacteria with grouped movement) to spawn the various bacteria coded previously.

Slow down the simulation speed and create different types of bacteria on different types of nutrients. You should be able to observe the different effects of the nutrients on the bacteria.

For example, in the video below, you can see that the single-flagellated bacterium finds it harder to consume the blue nutrients than the yellow ones, and that the bacteria with group movement are quickly "killed" by the blue nutrients:


Back to project description (part 5) Next module (step 5.2)