Goals: To ensure that a particular type of nutrient does not have the same effect on a particular type of bacterium.
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.
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.
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 bacteriain 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);
}
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:
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.
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: