-
Notifications
You must be signed in to change notification settings - Fork 514
Dependency Injection
This page contains some details about the purpose of the @Inject
annotations in AuthMe and how to use them.
@Inject
allows us to use inversion of control more easily.
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.
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.
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
.
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.
Dependencies can be passed to a class in different ways. It is important to only use one form per class.
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.
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.
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.