Skip to content

Dependency Injection

ljacqu edited this page Aug 5, 2016 · 3 revisions

This page contains some details about the purpose of the @Inject annotations in AuthMe and how to use them.

Purpose

@Inject allows us to use inversion of control more easily.

Inversion of control

In a nutshell, inversion of control means that we pass the services a class needs from the outside, instead of having the class instantiate or find (getInstance()) the service itself. This renders dependencies explicit (we are more aware of our service's dependencies), makes it easier to switch out a component in the future and facilitates unit testing.

Example

Consider the following class:

public class MessageTask {

    public void setTask(String name) {
        if (PlayerCache.getInstance().isAuthenticated(name) && LimboCache.getInstance().hasLimboPlayer(name)) {
            LimboCache.getInstance().getLimboPlayer(name).initMessageTask();
        }
    }
}

Applying inversion of control, the class could look as follows:

public class MessageTask {

    private final PlayerCache playerCache;
    private final LimboCache limboCache;

    public MessageTask(PlayerCache playerCache, LimboCache limboCache) {
        this.playerCache = playerCache;
        this.limboCache = limboCache;
    }

    public void setTask(String name) {
        if (playerCache.isAuthenticated(name) && limboCache.hasLimboPlayer(name)) {
            limboCache.getLimboPlayer(name).initMessageTask();
        }
    }
}

With the second version, we immediately see that MessageTask needs a LimboCache and PlayerCache instance. We can easily test the class by passing mock implementations, whereas in the first version we do not have the possibility to switch the implementation.

Dependency injection

Without dependency injection

One disadvantage with the "inversion of control" variant in the above code is that initializing the class becomes more difficult as we have to explicitly know about its dependencies (and in turn, of their dependencies). Consider the following code:

 private CommandHandler initializeCommandHandler(PermissionsManager permissionsManager, Messages messages,
                                                 PasswordSecurity passwordSecurity, NewSetting settings) {
    HelpProvider helpProvider = new HelpProvider(permissionsManager, settings.getProperty(HELP_HEADER));
    Set<CommandDescription> baseCommands = CommandInitializer.buildCommands();
    CommandMapper mapper = new CommandMapper(baseCommands, permissionsManager);
    CommandService commandService = new CommandService(
        this, mapper, helpProvider, messages, passwordSecurity, permissionsManager, settings);
    return new CommandHandler(commandService);
}

We want to instantiate a CommandHandler. We need to pass a CommandService to it, so we need to initialize this first. However, it requires a CommandMapper, a HelpProvider... and so forth. This forces us to deal with tons of classes (CommandService, CommandMapper, etc.) we do not care about directly—we just want to get a CommandHandler.

Purpose of dependency injection

Dependency injection allows us to avoid huge initialization blocks by injecting dependencies for us. We can simply request a class from the injector:

CommandHandler handler = injector.getSingleton(CommandHandler.class);

The injector will scan the CommandHandler class for its dependencies, instantiate them and pass them to the class for us. Basically, it does the big code chunk from above for us automatically. We need to help the injector a little bit by annotating the dependencies with @Inject in order to tell the injector what to inject.

Injection methods

Dependencies can be passed to a class in different ways. It is important to only use one form per class.

Constructor injection

A constructor can be annotated with @Inject. This tells the injector that the parameters of that constructor need to be injected. Consider the MessageTask class from the first example. We only need to add an annotation to enable constructor injection:

public class MessageTask {

    private final PlayerCache playerCache;
    private final LimboCache limboCache;

    @Inject
    MessageTask(PlayerCache playerCache, LimboCache limboCache) {
        this.playerCache = playerCache;
        this.limboCache = limboCache;
    }

    public void setTask(String name) {
        if (playerCache.isAuthenticated(name) && limboCache.hasLimboPlayer(name)) {
            limboCache.getLimboPlayer(name).initMessageTask();
        }
    }
}

Now a MessageTask singleton can be gotten via injector.getSingleton(MessageTask.class). The injector will notice that PlayerCache and LimboCache are required and will pass them to the constructor.

Field injection

Alternatively, fields can be annotated with @Inject:

public class MessageTask {

    @Inject
    private PlayerCache playerCache;
    @Inject
    private LimboCache limboCache;

    public void setTask(String name) {
        if (playerCache.isAuthenticated(name) && limboCache.hasLimboPlayer(name)) {
            limboCache.getLimboPlayer(name).initMessageTask();
        }
    }
}

Again, the injector will see the annotations on the fields and will set the fields. The fields can be private or have any other visibility. Field injection requires a no-arg constructor to be present.

@Inject gets singletons

No matter if you use constructor injection or field injection, it's important to understand that singletons will be passed, i.e. the injector will always pass the same implementation of the class. For example, if you have @Inject private LimboCache limboCache in some class and another @Inject private LimboCache limboCache in another class, both fields will have the same LimboCache object.