Lab: Subtype polymorphism#
Objective#
In this lab, we will refactor a simple program to implement subtype polymorphism. Starting with a Snake
class that interacts with apples and bombs in a non-polymorphic manner, we will introduce an interface to unify these interactions. Our goal is to deepen our understanding of polymorphism in object oriented programming and learn how to apply it to make our code more flexible and maintainable.
Provided Code#
We begin with a Snake
class that has methods to simulate eating an apple and a bomb, affecting its length and life status. This setup lacks polymorphism, which we will introduce to improve the design.
class Snake
{
public int Length { get; set; } = 1;
public bool IsAlive { get; set; } = true;
public void EatApple()
{
if (IsAlive)
Length++;
}
public void EatBomb()
=> IsAlive = false;
public void Print()
{
string status = IsAlive ? "Alive" : "Dead";
Console.WriteLine($"Snake: {Length} ({status})");
}
}
Snake snake = new Snake();
snake.EatApple();
snake.EatApple();
snake.EatApple();
snake.EatBomb();
snake.EatApple();
snake.Print();
Snake: 4 (Dead)
Instructions#
Let’s transform this code to implement subtype polymorphism by introducing an interface called IEdible
.
Step 1: Define the Interface#
Analyze the EatApple
and EatBomb
methods in the Snake
class. Notice how these methods have the same signature but different names. Also notice how both these methods require the same data.
Create an interface named IEdible
. This interface should define a method that encapsulates the action of the Snake
eating an object.
You might want to use the signature void GetEatenBy (Snake snake)
.
Step 2: Implement the Interface#
Create two classes called Apple
and Bomb
and let them that implement the IEdible
interface. Ensure that each class defines the appropriate behavior when eaten by the Snake
.
Your code should behave like this:
Snake snake = new Snake();
List<IEdible> edibles = new List<IEdible>() {
new Apple(),
new Apple(),
new Bomb(),
new Apple()
};
foreach (IEdible edible in edibles)
edible.GetEatenBy(snake);
snake.Print();
Snake: 4 (Dead)
Notice how we’ve removed the if
statement that checked if the Snake
was alive, before allowing it to eat the Apple
. We’ll reintroduce this check in the next step.
🤔 Reflection
How does implementing the IEdible
interface change the way we interact with the Apple
and Bomb
objects? Discuss the benefits of using an interface in this scenario.
Step 3: Refactor the Snake Class#
Modify the Snake
class to remove the EatApple
and EatBomb
methods. Instead, introduce a method with the signature void Eat(IEdible edible)
that allows the Snake
to interact with any IEdible
object.
In this step you should also reintroduce the if
statement that checks if the Snake
is allowed, so that only snakes that are alive can eat IEdible
s.
This check should reside in the method Eat
.
Your code should behave like this:
Snake snake = new Snake();
List<IEdible> edibles = new List<IEdible>() {
new Apple(),
new Apple(),
new Apple(),
new Bomb(),
new Apple()
};
foreach (IEdible edible in edibles)
snake.Eat(edible);
snake.Print();
Snake: 4 (Dead)
Notice how the Snake
’s state changes after each interaction.
🤔 Reflection
How does passing an IEdible
to a Snake
change the way we interact with the Apple
and Bomb
objects? Why did we introduce this method?
Step 4: Add GoldenApple
#
Add a new GoldenApple
class that implements IEdible
. This GoldenApple
should take an int
in its constructor which defines how much the Snake
should grow by when it is eaten.
Include it in your list of IEdible
objects and observe the outcome:
Snake snake = new Snake();
List<IEdible> edibles = new List<IEdible>() {
new Apple(),
new Apple(),
new GoldenApple(10),
new Bomb(),
new Apple(),
new GoldenApple(20)
};
foreach (IEdible edible in edibles)
snake.Eat(edible);
snake.Print();
Snake: 13 (Dead)
🤔 Reflection
Reflect on the process of adding the GoldenApple
to the system. How did the use of polymorphism make this addition easier and more seamless?
Challenge#
Add a class called MetalApple
that implements IEdible
. This apple should take two int
arguments in its constructor. The first defines how many times the apple has to be eaten before its effect is applied to the Snake
. The second defines how much the Snake
should grow by when the effect is finally applied. The Snake
that eats the MetalApple
the last time is the one that the effect is applied to.
It should behave like this:
Snake p1 = new Snake();
Snake p2 = new Snake();
MetalApple metalApple = new MetalApple(3, 100);
List<IEdible> edibles = new List<IEdible>() {
new Apple(),
new Apple(),
metalApple,
metalApple,
new GoldenApple(10),
new Bomb(),
new Apple(),
new GoldenApple(20)
};
foreach (IEdible edible in edibles)
p1.Eat(edible);
p2.Eat(metalApple);
p1.Print();
p2.Print();
Snake: 13 (Dead)
Snake: 101 (Alive)
Conclusion#
Through this exercise, you have gained practical experience in applying subtype polymorphism to make your object oriented programs more dynamic.
High-five ✋. Good job.