November 01 2021

The "KISS" Dependency Injection in Unity


Two questions for you:

  1. Are you using a dependency injection system in Unity?
  2. If so, can you understand, read, debug and maintain the code of your dependency injection system in less than 10 minutes?

If the answer to any is NO, this post might help :-)

datadriveninvestor.com

Why Use Dependency Injection in Unity?

Here’s one simple but (sadly) often ignored principle in software development: SRP.

That reads: Single Responsibility Principle.

And that means: every class, every function should have one and only one responsibility.

Some examples:

  • Class PlayerProfile. This class should only contain data related to the player (level, name) and functions directly related to reading/writing them.
    The PlayerProfile class should NOT show level-up popups nor grant xp-related achievements.
  • Function int CalculateGainedExperience(int level). This function should just calculate how much experience you gain when you beat a level.
    It should NOT grant xp to the player, nor write any data in the player profile. In fact, it should have no side effects. Ninguno.

This is a common mistake that violates SRP:

class Player
{
  void LevelUp()
  {
    Backend.Instance.SendLevelUp();
  }
}

You see that Backend.Instance access?

This access violates the Single-Responsibility Principle.

The LevelUp function should just do a level up. It is not part of its job description to find a reference to a backend system (in this case, the singleton).

So this function should just invoke the SendLevelUp function through a backend reference that someone passed it.

This is why breaking SRP in this manner is a problem:

  • The next time you change your specific backend implementation, gl hf changing all references.
  • Creating unit tests that mock the backend is hard, as you can’t easily change the references
  • Getting references makes your code messier and harder to read for everyone

More typical examples of SRP violation:

  • FindObjectOfType<Backend>()
  • GetComponent<Backend>()

I can sum it up this way: finding a reference to an object is not your class/function’s problem.

So, whose problem is it then?

Finding object references is the Dependency Injector’s problem.

What is Dependency Injection (DI)?

To keep it short, a DI system does two things:

  1. Keep track of your main project objects, e.g. Backend, VO, Localization, Logger
  2. Assign/Inject references into your objects as they require

Think of a DI system as the table of contents of a book. A ToC with a list of chapters ( Backend) and their page/location (0x123123).

This is how you 1. Keep track of your references:

Injector.Bind<IBackend>(new Backend());
Injector.Bind<ILogger>(new LoggerFile());
// ...

And this is how you 2. Inject your references into your objects:

class Player
{
  [Inject] private IBackend _backend;

  void LevelUp()
  {
    _backend.SendLevelUp();
  }
}
// ...
// ...
Injector.Inject(myPlayerObject);

That’s it.

When it comes to choosing a DI system, there are many third-party libraries out there like Zenject and StrangeIoC.

However, I have a big problem with most…

Woah! A 'Lightweight' framework, sure

My Main Problem With (Most) Existing Unity Dependency Injection Systems

The main problem I have is that they are not simple, but rather quite complex.

In other words, most DI implementations don’t follow the KISS approach (KISS = Keep it Simple Stupid).

It’s not uncommon to see DI systems with 20k lines of code or more (I’m looking at you, Zenject).

And this is a huge problem because of the effort and time it takes to:

  • Understand its system
  • Read its code
  • Maintain it
  • Debug it

I used Zenject a few times.

Can you guess what happened after running into problems?

I spent DAYS understanding and debugging its code base. Several times.

How much time did I save by using Zenject?

None. In fact, it was negative value. All in all, I spent more time working on a DI system than the DI system working for me.

Not great 😡

So, what to do about it?

My KISS Approach to Dependency Injection in Unity

Here’s a piece of advice that you can feel free to ignore (and eventually regret):

Every time you evaluate using a third-party library in your PRODUCTION-level project, investigate it well.

Ask yourself these questions about any library you want to use:

  1. Can I understand what it does without reading its documentation within 10 minutes?
  2. Can I effectively read most of its code within these 10 minutes?
  3. Could I easily debug it whenever I run into problems within 10 minutes?

It might be 10 minutes, it might be 1 hour. It depends on the system we’re talking about.

At its essence, the key part is this: you want to Keep It Simple Stupid.

Ever since I spent days working on a DI system rather than the DI system working for me, I adhere pretty much to the KISS rule whenever I can.

Sure, that’s not always the case with libraries like Photon or engines like Unity, but those are big for a reason.

A Dependency Injection system can be and should be simple.

Again, simple means:

  • You can easily understand it
  • You can quickly debug it when problems arise
  • You can simply extend it if you really have to

Following the KISS principle is a professional matter.

So no more bells and whistles, thanks.

What’s Next?

You can watch the full lesson on my approach to dependency injection on the module 2021.10 of the PerformanceTaskforce.

This lesson includes everything you need to quickly get started:

  • A proper KISS DI system (1 file, < 150 lines)
  • Examples
  • Live QA discussion

Enroll in the Unity Performance Taskforce and head to module 2021.10.

(& grab some popcorn :-))

Ruben

The Gamedev Guru Logo

Performance Labs SL
Paseo de la Castellana 194, Ground Floor B
28046 Madrid, Spain

This website is not sponsored by or affiliated with Facebook, Unity Technologies, Gamedev.net or Gamasutra.

The content you find here is based on my own opinions. Use this information at your own risk.
Some icons provided by Icons8