D20 RPG – Interface Injection

It’s a bird, no it’s a plane, no – it’s interface injection!

Overview

Some dependencies may need to be injected dynamically as you go. Others may be able to inject themselves. Most of them however, will only need to be injected once – at the beginning of the application’s lifecycle.

For most of this project series so far, we have really only needed to inject anything when we were making Unit Tests. In the last lesson, our little project grew up enough to actually need to start injecting things in the AppFlow script. While we “could” just add each and every dependency one at a time to the same script, I wouldn’t recommend it. One concern is that the number of dependencies is likely to grow quite large.

A better approach is to make classes whose whole job is just to inject certain collections of dependencies. This should help keep any particular script from growing too large, and it also may help things be reusable, such as when setting up a test.

Getting Started

Feel free to continue from where we left off, or download and use this project here. It has everything we did in the previous lesson ready to go.

Interface Injection

In the overview, I mentioned that we would have separate classes focusing on collections of dependencies. Each class will have no purpose beyond the initial step of registering “things” – each to their relevant interface. Therefore, it makes sense for these classes to just be static classes with static methods. It is almost like having global functions.

We can approach this with a sort of hierarchical setup for the different features. In fact, it makes sense to structure it the same way I have added the sub-folders within the Scripts folder. For example, I can see that Component has systems that will need to be injected, and that it has another subfolder AbilityScore which also has systems that need to be injected. We can start there.

Create a new script at Assets -> Scripts -> Component -> AbilityScore named AbilityScoreInjector and copy the following:

public static class AbilityScoreInjector
{
    public static void Inject()
    {
        IAbilityScoreSystem.Register(new AbilityScoreSystem());
        ICharismaSystem.Register(new CharismaSystem());
        IConstitutionSystem.Register(new ConstitutionSystem());
        IDexteritySystem.Register(new DexteritySystem());
        IIntelligenceSystem.Register(new IntelligenceSystem());
        IStrengthSystem.Register(new StrengthSystem());
        IWisdomSystem.Register(new WisdomSystem());
    }
}

This is a static class, with a static method. You can’t create instances of static classes, and you use static methods through the class itself. You’ll see how to do this soon. The injector class registers each of the systems in the folder. They are listed alphabetically, but that isn’t a requirement, it simply helps find dependencies in case the list started to grow large.

Create a new script at Assets -> Scripts -> Component named ComponentInjector and copy the following:

public static class ComponentInjector
{
    public static void Inject()
    {
        AbilityScoreInjector.Inject();
        INameSystem.Register(new NameSystem());
    }
}

Notice that this injector handles any systems at its own hierarchy level and below, and for those below, it relies on the subfolder level injector.

Create a new script at Assets -> Scripts -> Data named DataInjector and add the following:

public static class DataInjector
{
    public static void Inject()
    {
        IDataSerializer.Register(new DataSerializer());
        IDataStore.Register(new DataStore("GameData"));
        IDataSystem.Register(new DataSystem());
    }
}

Create a new script at Assets -> Scripts -> DiceRoll named DiceRollInjector and add the following:

public static class DiceRollInjector
{
    public static void Inject()
    {
        IDiceRollSystem.Register(new DiceRollSystem());
        IRandomNumberGenerator.Register(new RandomNumberGenerator());
    }
}

Create a new script at Assets -> Scripts -> Flow named FlowInjector and add the following:

public static class FlowInjector
{
    public static void Inject()
    {
        IMainMenuFlow.Register(new MainMenuFlow());
    }
}

Note that for the FlowInjector, I intentionally did not add the AppFlow. Even “if” I made it conform to an injectable interface, it is implemented as a MonoBehaviour and so would be able to inject itself just like the MainMenu script does.

Finally we can make a script for the root level injector. Create a new script at Assets -> Scripts -> Dependency named Injector and add the following:

public static class Injector
{
    public static void Inject()
    {
        ComponentInjector.Inject();
        DataInjector.Inject();
        DiceRollInjector.Inject();
        IEntitySystem.Register(new EntitySystem());
        FlowInjector.Inject();
    }
}

In the future, as we add new systems (or anything that is injectable) we should follow the same basic pattern as we have done here.

App Flow

Open the AppFlow script. Replace these lines:

IMainMenuFlow.Register(new MainMenuFlow());
IDataSerializer.Register(new DataSerializer());
IDataStore.Register(new DataStore("GameData"));
IDataSystem.Register(new DataSystem());

With this single line:

Injector.Inject();

Although you won’t see any functional difference since the previous lesson, we will now have access to every system we have created so far.

Summary

In this short but sweet lesson, we setup the pattern we will be using for interface injection. All of the injectable systems are now accounted for, though this is something we will have to continue to update as we add more systems over time.

If you got stuck along the way, feel free to download the finished project for this lesson here.

If you find value in my blog, you can support its continued development by becoming my patron. Visit my Patreon page here. Thanks!

Leave a Reply

Your email address will not be published. Required fields are marked *