SOLID – Dependency Inversion

High-Level classes (i.e.PlayerController) should not be directly dependant on specific Low-Level classes (i.e. EnemyBullet, EnemyRocket, EnemyLaser, …). Instead these High-Level Classes should be dependant on abstractions of these classes i.e. an Interface IProjectile.

So in a nutshell:
Use Interfaces to reduce dependency on specific classes.

Let’s have a look at an Example:

Normal Version

Here we can see a simple lever, which can be pressed to open a Door. While this works perfectly fine, you may want to also use this lever to turn on a light or blow up a bomb. With the current implementation this is simply not possible because the lever is directly connected to a Door.

class Lever
{
  Door _door;

  void Activate()
  {
    _door.Open();
  }
}

Dependency Inversion

With this version of our Lever we can however interact with any object that implements the IInteractable-Interface. So this Lever is now way more versatile.
This however is only the tip of the iceberg. Read the follow-up Blog-Post to learn more about other important Applications for this Principle.

interface IInteractable
{
  void Interact();
}

class Lever
{
  IInteractable _interactable;

  void Activate()
  {
    _interactable.Interact();
  }
}

Keeping the Code extensible

The Dependency Inversion Principle also helps a great deal in keeping the code extensible without having to modify it (Open-Close-Principle).

Let’s have a look at Code I see very often from Junior Programmers:

class PlayerController : MonoBehaviour
{
  int _hp = 100;

  void OnTriggerEnter(Collider other)
  {
    Bullet bullet = other.GetComponent<Bullet>();
    Rocket rocket = other.GetComponent<Rocket>();
    PlasmaBurst plasmaBurst = other.GetComponent<PlasmaBurst>();
    if(bullet != null)
    {
      _hp -= bullet._damage;
    }
    if(rocket != null)
    {
      _hp -= rocket._damage;
    }
    if(plasmaBurst != null)
    {
      _hp -= plasmaBurst._damage;
    }
  }
}

Firstly I want you to think through what you’d have to do in this class everytime you create a new Projectile-Type.
And then let’s assume we have 5 more classes that need to react on these Projectiles.

Chances are, that when you want to create the new weapon-type “LaserBlast” you will forget about adding it to one of these 6 classes that check for projectiles.

The better way of implementing this is by using an Interface to apply the Dependency-Inversion-Principle:

class PlayerController : MonoBehaviour
{
  int _hp = 100;

  void OnTriggerEnter(Collider other)
  {
    IProjectile projectile = other.GetComponent<IProjectile>();
    if(projectile != null)
    {
      _hp -= projectile ._Damage;
    }
  }
}

That way the code becomes shorter, easier to read and more extensible because even if we add another weapon-type we don’t need to touch the PlayerController at all as long as the new Weapon-Type implements the IProjectile – Interface.

This example exemplifies why it makes sense to hide low-level classes behind abstractions

Icons created by Freepik – Flaticon