How to Explain Dependency Injection to a 6-Year-Old Kid

Albert Einstein famously remarked, “If you can’t explain it to a six-year-old, you don’t understand it yourself.” So, faced with the challenge of explaining Dependency Injection (DI) to a curious 6-year-old, how might we approach it?

“Imagine you have a collection of action figures: knights, kings, magicians, and so on. DI is akin to providing these figures with interchangeable tools or weapons, such as swords or magic wands. The beauty is that these weapons aren’t permanently attached to any specific figure. This flexibility means you can arm the knight with a magic wand one day or perhaps let the magician wield a war hammer the next.”

Here’s an added layer of intrigue: the toy company producing these figures doesn’t always know in advance what new weapons they might release in the future. Whether it’s influenced by market demand or innovative design, they’re always thinking ahead. However, they ensure one thing remains consistent: the design of the action figure’s hand. This standardized grip ensures that no matter what exciting weapon they introduce next — be it a Gatling gun or a futuristic particle beam gun — your action figures will always be ready to wield it.

Dependency Injection for Kid

After hearing that fun analogy, the young learner might be buzzing with excitement about programming. So, to keep that momentum going, let’s roll up our sleeves and dive into some coding! We’ll kick things off with a straightforward example without any fancy DI magic. This will give us a chance to spot the potential hurdles and limitations of such an approach. And once we’ve identified the problems, we’ll let our hero, DI, swoop in and save the day.

To set the stage, we’ve crafted a Sword and Shield class. These act as the trusty tools for our valiant knight. Delve a bit deeper, and you'll notice each class has either an Attack() or Defend() function. These functions spit out a value, showcasing the might (or resilience) of the given tool.

public class Sword
{
    public int Attack() => 70;
}

public class Shield
{
    public int Defend() => 30;
}

Then, our Knight class.

public class Knight
 {
     private Sword _sword; 
     private Shield _shield;
     

     public string Name { get; set; }
     public int HP { get; private set; } = 100;

     public Knight(string name, Sword sword, Shield shield)
     {
         Name = name;
         _sword = sword;
         _shield = shield;
         
     }

  public int Attack()
  {
      int damage = _sword.Attack();
      Console.Write($"{Name} is attacking now, ");
      Console.WriteLine($"damage casted is {damage}");
      return damage;
  }

  public void Defend(int damage)
  {
      int actualDamage = damage - _shield.Defend();

      Console.WriteLine($"{Name} is taking damage {actualDamage}");

      if(actualDamage>0)
      HP -= actualDamage;

      if (HP > 0)
      {
          Console.WriteLine($"{Name} remained {HP} HP ");
      }
      else
      {
          Console.WriteLine($"{Name} has fallen.");
      }

      Console.WriteLine();
  }
}

The Knight class contains two properties: Name and HP. Additionally, it has two primary methods: Attack() and Defend(). The Attack() method calls upon the Sword class to determine the attack value, while the Defend() method utilizes the Shield class to mitigate the damage received.

Now, let’s move on to the main class, where our knights will engage in a duel to demonstrate these functionalities.

Sword sword = new Sword();
Shield shield = new Shield();

Knight Jamie = new Knight("Jamie The King Slayer", sword, shield);
Knight Aerys = new Knight("Aerys II The Mad King", sword, shield);

while (Jamie.HP > 0 && Aerys.HP > 0) //pretends this is a game play
{
    int damage = Jamie.Attack();
    Aerys.Defend(damage);

    if (Aerys.HP <= 0) continue;

    damage = Aerys.Attack();
    Jamie.Defend(damage);
}

Example Output

Until now, everything has worked fine. In the main program, we created objects for the Sword and Shield and then passed those two objects to instantiate two Knight class objects, allowing them to duel to death. I now wish to introduce advanced weaponry to our knight battle game.

Firstly, I’d like to replace the standard sword attack with a magical one, such as a ‘magic fire’ attack. Additionally, I want to introduce a powerful sword equipped with a special ability to deal extra critical damage based on certain odds. But it’s not just about offense. I also aim to incorporate a magical shield, which, under specific probabilities, can render our hero invincible. With this in mind, I’ve created the following new classes:

public class MagicFire
{
    public int Attack() => 40;
}

public class SwordOfTheMorning
{
    public int Attack()
    { 
        Random random = new Random();
        if (random.Next(1, 101) > 70) // 30% rate to get high damage 
        {
            Console.WriteLine("Sword of the morning cast critical damage!!!");
            return 100;
        }
        return 50;
    }
}

public class MagicShield
{
    public int Defend()
    {
        Random random = new Random();
        if (random.Next(1, 101) > 70) // 30% rate to get 1000 defence power
        {
            return 1000;
        }
        return 30;
    }
}

Now that I have a new type of sword and shield, my existing constructor, which takes a regular Sword and Shield, is no longer suitable for handling my new weapons.

public Knight(string name, Sword sword, Shield shield)
{
    Name = name;
    _sword = sword;
    _shield = shield;
}

So, I have to create new constructors to cater for my new classes.

public Knight(string name, SwordOfTheMorning swordOfTheMorning, Shield shield)
{
    Name = name;
    _swordofthemorning = swordOfTheMorning;
    _shield = shield;
}

public Knight(string name, MagicFire magicFire, MagicShield magicShield)
{
    Name = name;
    _magicFire = magicFire;
    _magicShield = magicShield;
}

Actually, my two new constructors are inadequate, even for my simple demo. We may encounter scenarios where a knight possesses both the Sword Of The Morning and the Magic Shield simultaneously, or a standard Sword with a Magic Shield, or Magic Fire with an ordinary shield, and so forth.

Not only are the constructors no longer suitable, but our existing Attack() and Defend() methods, which were designed to create objects of Sword and Shield, also cannot accommodate the new functionalities introduced by our advanced weapons. As a result, I need to devise new methods to invoke the functionalities from our newly introduced classes.

public int AttackWithSwordOfTheMorning()
{
    int damage = _swordofthemorning.Attack();
    Console.Write($"{Name} is attacking with Sword Of The Morning now, ");
    Console.WriteLine($"damage casted is {damage}");
    return damage;
}

public int AttackWithMagicFire()
{
    int damage = _magicFire.Attack();
    Console.Write($"{Name} is attacking with Magic Fire now, ");
    Console.WriteLine($"damage casted is {damage}");
    return damage;
}

public void DefendWithMagicSheild(int damage)
{
    int actualDamage = damage - _magicShield.Defend();

    Console.WriteLine($"{Name} is taking damage {actualDamage}");

    if (actualDamage > 0)
    {
        HP -= actualDamage;
    }
    else
    {
        Console.WriteLine($"Magic Shield prevents {Name} from being hit!");
    }

    if (HP > 0)
    {
        Console.WriteLine($"{Name} remained {HP} HP ");
    }
    else
    {
        Console.WriteLine($"{Name} has fallen.");
    }

    Console.WriteLine();
}

In the main class

SwordOfTheMorning swordOfTheMorning = new SwordOfTheMorning();
MagicFire magicFire = new MagicFire();
MagicShield magicShield = new MagicShield();

Knight ArthurDayne = new Knight("Sir Arthur Dayne", swordOfTheMorning, shield);
Knight Melisandre = new Knight("Melisandre", magicFire, magicShield);

while (ArthurDayne.HP > 0 && Melisandre.HP > 0) // Pretends this is a gameplay
{
    int damage = ArthurDayne.AttackWithSwordOfTheMorning();
    Melisandre.DefendWithMagicShield(damage);

    if (Melisandre.HP <= 0) continue;

    damage = Melisandre.AttackWithMagicFire();
    ArthurDayne.Defend(damage);
}

Console.ReadLine();

Actually, I simplified the code by assuming we knew which weapon would be assigned to our knight. Therefore, I just used the appropriate constructor and invoked the correct method. However, in a real-world scenario, the equipment a knight uses isn’t static. We would need to conduct extensive checks, in my situation, to determine the weapon a knight holds before invoking the correct Attack() method.

The complexity will only amplify. Imagine if I decided to introduce 10 more types of swords, shields, and magical attacks.

Let's revisit our analogy to a 6-year-old kid. Our current predicament, without Dependency Injection (DI), is akin to every time we wish to equip a new weapon to our knight action figure, we must alter or change the figure’s hand, or even its chest or head, just so it can accommodate the new weapon.

What we aspire to design is an action figure that’s independent of the weapon — a system where weapons can be seamlessly attached or detached, much like plug-and-play. Translating this to programming, we need to employ the DI principle to overhaul my existing code. The first step in this process is to utilize interfaces for standardization.

public interface IWeapon
{
    int Attack();
}

public interface IShield
{
    int Defend();
}

For offensive capabilities, I employ the IWeapon interface, and for defense, I use the IShield interface. Additionally, I've established an IHero interface for our Knight. The next step is to have our weapons and knights implement these interfaces.

public class Sword : IWeapon
{
    public int Attack() => 70;
    
}
  public class SwordOfTheMorning : IWeapon
  {

      public int Attack()
      { 
          Random random = new Random();
          if (random.Next(1, 101) > 70)//30% rate to get high damage 
          {
              Console.WriteLine("Sword of the morning cast critical damage!!!");
              return 100;
          }
          return 50;
      }
  }
 public class MagicShield: IShield
 {

     public int Defend()
     {
         Random random = new Random();
        
         if (random.Next(1, 101) > 70)//30% rate to get 1000 defence power
         {
             return 1000;
         }

         return 30;
     }
 }

// and so on... 

Given that we’ve introduced magical attacks, using just the Knight class might not be entirely appropriate. We could have other roles like Magician, Healer, and so on. To accommodate this, I’ve created an IHero interface, allowing the Knight and other hero-related classes to implement it.

Pay close attention to the constructor of my Knight class. It accepts the param with datatype IWeapon and IShield. This means we can not fix our constructor to accept concrete Sword or Shield class objects, but any object that implemented IWeapon and IShield. This gave us great flexibility to create different kinds of weapons and pass them to our knight without any extra code changes.

public interface IHero
{
    string Name { get; set; }
    int HP { get; set; }

    int Attack();
    void Defend(int damage);
}

public class Knight : IHero
{
    private IWeapon _weapon;
    private IShield _shield;

    public string Name { get; set; }
    public int HP { get; set; } = 100;

    public Knight(string name, IWeapon weapon, IShield shield)
    {
        Name = name;
        _weapon = weapon;
        _shield = shield;
    }
}

Additionally, by using the IWeapon and IShield interfaces to generalize our weapon and shield classes, we eliminate the need to create separate Attack or Defend classes. These two interfaces alone will suffice.

public int Attack()
{
    int damage = _weapon.Attack();
    // Omit some codes...
}

public void Defend(int damage)
{
    int actualDamage = damage - _shield.Defend();
    // Omit some codes...
}

In the main class, it’s evident that regardless of the type of sword, shield, or magical power, all our Knight objects utilize the same Attack() and Defend() methods. There's no longer a need to verify the weapon type to call a specific attack method.

//Ordinarily Sword or Shield
Sword sword = new Sword();
Shield shield = new Shield();

Knight Jamie = new Knight("Jamie The King Slayer", sword, shield);
Knight Aerys = new Knight("Aerys II The Mad King", sword, shield);

while (Jamie.HP > 0 && Aerys.HP > 0) // Pretend this is gameplay
{
    int damage = Jamie.Attack();
    Aerys.Defend(damage);

    if (Aerys.HP <= 0) continue;

    damage = Aerys.Attack();
    Jamie.Defend(damage);
}

//Special Sword or Shield
SwordOfTheMorning swordOfTheMorning = new SwordOfTheMorning();
MagicFire magicFire = new MagicFire();
MagicShield magicShield = new MagicShield();

Knight ArthurDayne = new Knight("Sir Arthur Dayne", swordOfTheMorning, shield);
Knight Melisandre = new Knight("Melisandre", magicFire, magicShield);

while (ArthurDayne.HP > 0 && Melisandre.HP > 0) // Pretend this is gameplay
{
    int damage = ArthurDayne.Attack();
    Melisandre.Defend(damage);

    if (Melisandre.HP <= 0) continue;

    damage = Melisandre.Attack();
    ArthurDayne.Defend(damage);
}

By implementing Dependency Injection (DI), we can now effortlessly introduce new weapons or special abilities to our knight object. Each addition can be made without having to alter our Knight class. This is analogous to our action figure being able to equip various types of weapons through a simple plug-and-play mechanism without the need to modify any part of the figure itself!

Property Injection and Method Injection

So far, what I’ve demonstrated is Constructor Injection. Now, I’d like to delve into Property Injection (PI) and Method Injection (MI).

Let’s start with PI. We look back to our Knight class again. This time, I created a new property named Potion.

//Interface of Potion  
public interface IPotion
  {
      int Heal();
  }    

//in Knight Class 
public IPotion Potion { get; set; }

Property Injection can be achieved via the following.

//in main class
     Melisandre.Potion = new MagicPotion();

You might be wondering why I need to use PI. Why don’t I just inject this in the constructor of the Knight class, like the following?

public Knight(string name, IWeapon weapon, IShield shield, IPotion potion)
{
    // ....
    Potion = potion;
    // ...
}

Well, there are several reasons. One primary reason is that I don’t want to enforce a stringent contract like Constructor Injection (CI). Take the potion in a game, for instance. It’s optional. The character might not always find it. If the property were as crucial as, say, a name in my example, then I’d opt for constructor injection. From a coding standpoint, I would check if my knight has a potion. If he does, his health will regenerate.

//In the Defend function
if (Potion != null && HP > 0)
 {
     int healPoint = Potion.Heal();
     HP += healPoint;
     Console.WriteLine($"{Name}'s HP regenate {healPoint} points ");
     Console.WriteLine($"{Name} remained {HP} HP ");
 }

For Method Injection, let's say now I create another method for my Knight class, named TagTeamAttack(). This function will accept one object under the IHero interface, and the attack will be a combination of two heroes. The function will look like this.

public int TagTeamAttack(IHero allyHero)
{
    int damage = _weapon.Attack();
    int allyHeroDamage = allyHero.Attack();

    Console.Write($"{Name} and {allyHero.Name} are attacking now, ");
    damage += allyHeroDamage;
    Console.WriteLine($"total damage casted are {damage}");
    return damage;
}

//in the main function

int damage = ArthurDayne.TagTeamAttack(Jamie); // Optional

As you can see, the function expects an object that adheres to the IHero interface. Upon receiving the object, it simply retrieves the damage from that ally hero without concerning itself with how that specific hero executes the attack. Each hero has a unique attack method, but that's not our main focus. We merely harness their attack, provided the object conforms to the required interface.

A common real-life example of MI involves passing logging or database services as parameters. Consider the sample code below.

public int SaveData(IDatabase myDatabase, string data)
  {
      myDatabase.Save(data);//let the instance of myDatabase do the saving job

//instead of concrete coding of how to save into database A/B/C
}

This mock SaveData the method incorporates MI in its design. As observed, it requires an IDatabase object and will save the content of the 'data' variable. A significant advantage of using MI is that, within the same project, different data sets might need saving to various database services. Rather than hardcoding the logic for saving to DB A, B, or C directly within the method, we simply pass the appropriate database service (be it DB A, B, or C) to the SaveData method and let it handle the task. The SaveData() method then determines which database (A, B, or C) to save to based on the parameters provided.

Abstract Factory

While the Abstract Factory pattern falls outside the scope of this DI article, I’d like to briefly touch on it due to its close relationship with the concept of loose coupling.

Consider my HeroFactory class, which functions as a factory to produce our heroes. Given that both our Knight and Magician classes implement the IHero interface, this HeroFactory is capable of creating objects from either the Knight or Magician class.

 public class HeroFactory
 {
     public IHero CreateHero(int type, string name, IWeapon weapon, IShield shield = null)
     {
         switch (type)
         {
             case 1: return new Knight(name, weapon, shield);
             case 2: return new Magician(name, weapon, shield);
             default: throw new ArgumentException("Invalid hero type.");
         }
     }
 }

public class Knight : IHero
{//...

  public class Magician : IHero
  {//...

Creating different hero classes is straightforward. Here’s the code illustrating how to instantiate various heroes by supplying the appropriate parameters in the main class.

  IHero Jamie = heroFactory.CreateHero(1, "Jamie The King Slayer", sword, shield);
  IHero Aerys = heroFactory.CreateHero(1, "Aerys II The Mad King", sword, shield);

 IHero ArthurDayne = heroFactory.CreateHero(1, "Sir Arthur Dayne", swordOfTheMorning, shield);
 IHero Melisandre = heroFactory.CreateHero(2, "Melisandre", Magicfire, magicShield);

//... The rest of the code

If you’re interested in diving deeper into the Abstract Factory pattern, I invite you to read my article titled C# Abstract Factory Design Pattern With Code Example.

Conclusion

In essence, Dependency Injection (DI) ensures that our software, much like our action figures, remains adaptable and future-ready.DI lets us introduce new functionalities without overhauling our core system. So, the next time you’re faced with evolving requirements or innovative changes, remember the action figures and their ever-ready grip.

Additionally, the source code for this DI sample is available on my Github.


Similar Articles