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