Lab: Composition over Inheritance#
Objective#
Refactor the provided inheritance-based code to leverage Composition over inheritance, aiming for a more flexible, maintainable, and logically coherent representation of entities.
Provided code#
Study the starting code given to you.
Notice how the three classes Human
, Fish
, and Mermaid
all inherit from the class Animal
.
Then notice how the Main
method defines a local function that takes an object of type Animal
which, through subtype polymorphism, allows us to call the instance methods of said animal without knowing whether it’s a Human
, Fish
, or Mermaid
.
abstract class Animal
{
public abstract string Eat();
public abstract string Sleep();
public abstract string Move();
}
class Human : Animal
{
public override string Eat() => "Eating sea and land food.";
public override string Sleep() => "Sleeping.";
public override string Move() => "Walking.";
}
class Fish : Animal
{
public override string Eat() => "Eating sea food.";
public override string Sleep() => "Resting.";
public override string Move() => "Swimming.";
}
class Mermaid : Animal
{
public override string Eat() => "Eating sea and land food.";
public override string Sleep() => "Sleeping.";
public override string Move() => "Swimming.";
}
void TestAnimal(Animal animal)
=> Console.WriteLine($"{animal.Move()} | {animal.Eat()} | {animal.Sleep()}");
TestAnimal(new Human());
TestAnimal(new Fish());
TestAnimal(new Mermaid());
Walking. | Eating sea and land food. | Sleeping.
Swimming. | Eating sea food. | Resting.
Swimming. | Eating sea and land food. | Sleeping.
Instructions#
Step 1: Identify behaviors#
Identify which behaviors are shared and which are different among the classes. These should include eating, sleeping, and moving.
🤔 Reflection
Why can we not use simple inheritance to eliminate the duplication? Or even if we can, why would that be a bad idea?
Step 2: Define behavior interfaces#
Create separate interfaces for each identified behavior. For instance, IEatingBehavior
for eating.
🤔 Reflection
How does defining behavior through interfaces contribute to code flexibility and maintainability?
Step 3: Implement concrete behaviors#
Implement specific classes for each behavior interface. For example, you could have Sleeping
and Resting
as implementations of IEatingBehavior
.
Step 3: Compose classes through dependency injection#
Remove the old classes Human
, Fish
, and Mermaid
and replace them with the class called Animal
.
The class Animal
should no longer be abstract.
The three types of behavior that a given animal uses should be dependency injected through the constructor of Animal
.
🤔 Reflection
How does composition of behaviors help in representing real-world entities more accurately compared to inheritance?
Step 4: Instantiate and test#
Instantiate objects for the refactored classes and test their behaviors.
The Main
method should now look like the code below and the output should be the same as before.
Animal human = new Animal(new SeaAndLandFoodEating(), new Sleeping(), new Walking());
Animal fish = new Animal(new SeaFoodEating(), new Resting(), new Swimming());
Animal mermaid = new Animal(new SeaAndLandFoodEating(), new Sleeping(), new Swimming());
TestAnimal(human);
TestAnimal(fish);
TestAnimal(mermaid);
Walking. | Eating sea and land food. | Sleeping.
Swimming. | Eating sea food. | Resting.
Swimming. | Eating sea and land food. | Sleeping.
🤔 Reflection
Notice how behaviors are now composed and can be mixed and matched as needed. What are the benefits of this?
Step 5: Add your own behaviors and animals#
Add a new behavior class, such as Flying
, for each behavior type.
Then instantiate two new Animal
objects that use these types along with some of your existing types.
🤔 Reflection
How easy would it have been to add this additional behavior using the design that we had in the beginning?