Lab: Dependency injection#

Objective#

In this lab, we will delve into the practical application of Dependency Injection. We’ll refactor some simlpe code to implement dependency injection, enhancing its flexibility and maintainability.

This exercise demonstrates how dependency injection facilitates the addition of new features without modifying existing code, a core principle of maintainable software architecture.

Provided Code#

Carefully review the provided code. Notice how the Player class currently handles jump behaviors internally, leading to tight coupling between the Player and its abilities. This design limits flexibility and violates the Single Responsibility Principle, as the Player class is tasked with multiple responsibilities.

class Player
{
    bool hasDoubleJumpPowerUp = false;

    public void MoveRight()
        => Console.WriteLine("Moving right.");

    public void MoveLeft()
        => Console.WriteLine("Moving left.");

    public void Jump()
    {
        if (hasDoubleJumpPowerUp)
            Console.WriteLine("Performing double jump!");
        else
            Console.WriteLine("Performing regular jump!");

    }

    public void EnableDoubleJump(bool enabled)
        => hasDoubleJumpPowerUp = enabled;
}

Instructions#

We will refactor the Player class to use dependency injection for handling jump behaviors, thus decoupling the abilities from the player itself.

Step 1: Define Jump Behaviors#

First, define an interface called IJumpBehavior with a single method with the signature void Execute(). Then define two concrete implementations of the interface: SingleJump and DoubleJump. This will encapsulate the jump behaviors.

When you’re done, you should be able to run the following code.

IJumpBehavior b1 = new SingleJump();
IJumpBehavior b2 = new DoubleJump();

b1.Execute();
b2.Execute();
Performing regular jump.
Performing double jump.

Step 2: Refactor the Player Class#

Modify the Player class to accept an IJumpBehavior object through its constructor (constructor injection) and a method (method injection).

When you’re done you should be able to run the following code that creates a Player instance with a SingleJump behavior, calls Jump(), and then switches to DoubleJump using method injection.

IJumpBehavior basicJump = new SingleJump();
Player player = new Player(basicJump);
player.Jump();

IJumpBehavior doubleJump = new DoubleJump();
player.SetJumpBehavior(doubleJump);
player.Jump();
Performing regular jump.
Performing double jump.

🤔 Reflection

How does refactoring the Player class to use dependency injection improve its design? Consider principles like Single Responsibility and Open/Closed.

Challenge#

Add a new jump behavior, GlideJump, that represents a jump followed by a glide. Implement this behavior and inject it into a Player instance. Reflect on how the current design made adding this new feature easier compared to the original design.

When you’re done, you should be able to run the following code.

Player player = new Player(new SingleJump());
player.Jump();

player.SetJumpBehavior(new DoubleJump());
player.Jump();

player.SetJumpBehavior(new GlideJump());
player.Jump();
Performing regular jump.
Performing double jump.
Performing jump and glide.

🤔 Reflection

Reflect on how dependency injection has enabled easier extension of the Player class’s capabilities. Would adding this new behavior have been as straightforward without dependency injection?