Guice Stories ( Part 1)
If you use Bootique, then you know Google Guice is an important part of it. Make sure you know how to use it right to keep your modules and DI clean and happy.
Join the DZone community and get the full member experience.
Join For FreeI’ve always been a fan of Google Guice. More so lately, as Guice became an indispensable part of Bootique.io , responsible for Bootique's dependency injection (DI) and modularity. Guice is a simple, fun, and powerful DI engine with a number of advantages over the industry status quo, yet there's not a lot of guidance out there on its proper use compared to, say, Spring. To partially fill this gap, I wrote a few "stories", each describing a coding or a design task, then going through Guice solutions, from the most obvious to the most optimal. I hope this will be useful to developers who use either Guice or Bootique. This post starts with two stories. More will follow.
Story 1: Injection Hygiene
This story is about basic injection. We assume some services have been already "bound" in Guice and we want to use them to write our own "service". The simplest way to get a hold of another service is via field injection:
public class A {
@Inject
private B b;
}
This is a quick and dirty approach. It is obvious why I am calling it quick: We declare an instance variable and annotate it with @Inject
. Very little code. Why is it dirty though? Well, we ended up with a class that has a hard dependency on the injection container. There's no way (short of ugly reflection) to create instances of A
without Guice. So you won't be able to write unit tests, etc. An obvious refactoring is to create a public constructor and use constructor injection:
public class A {
private B b;
@Inject
public A(B b) {
this.b = b;
}
}
Much better — now both Guice and our own code will be able to create instances of A
. This is good enough and we can stop here, or we can perfect our solution a bit more. Notice that we still have a compile dependency on the @Inject
annotation. I don't know about you, but annotations on domain objects always look like a code smell to me. Let's refactor this to a pure object, moving the construction code into a "provider" method inside our Guice module:
public class A {
private B b;
public A(B b) {
this.b = b;
}
}
public class MyModule implements Module {
@Singleton
@Provides
A provideA(B b) {
return new A(b);
}
}
We ended up with a bit more code, but this code is arguably higher quality than what we had initially. And in a real app with many DI-managed objects of some complexity, such a trade-off will actually pay off.
There are other reasons as well why constructor injection may not be ideal, and the provider approach can save us. Some objects can have too many dependencies, so the argument list becomes unwieldy. (Yes, I am aware of setter injection, but that breaks object immutability, so let's not go there). Also, many objects require complex initialization logic. Using provider methods (or custom Provider classes) as a default choice keeps you from a temptation to stick factory code inside the constructor, and thus greatly reduces domain object coupling.
Story 2: Open/Closed Principle for Modules
I think the Injection story was pretty straightforward. Now let's talk a bit about modules and collections. If you've been using Guice, you are likely familiar with Multibinder
and MapBinder
that allow us to declare injectable maps and collections. What does it really give us? Let's consider an example. Say we have an imaginary Command
interface, and also an imaginary CommandExecutor
that looks up a command by name and then executes it:
public interface Command {
String exec();
}
public interface CommandExecutor {
String exec(String commandName);
}
Let's define a few commands and write a provider method for our executor:
public class CommandA implements Command {
public String exec() {
return "a_result";
}
}
public class CommandB implements Command {
public String exec() {
return "b_result";
}
}
public class MyModule implements Module {
@Provides
@Singleton
CommandExecutor provideCommandExecutor(CommandA ca, CommandB cb) {
Map<String, Command> commands = new HashMap<>();
commands.put("a", ca);
commands.put("b", cb);
return commandName -> commands.get(commandName).exec();
}
}
This code will work. There's one problem with it though. The module that defines CommandExecutor
will need to know all the commands upfront. This violates the open/closed principle, or in simpler terms, it does not allow us to create a reusable module with CommandExecutor
, deferring command definitions to app-specific downstream modules. Changing this code to use MapBinder
to collect commands cleanly solves the reusability problem:
public class ReusableModule implements Module {
public static MapBinder<String, Command> contributeCommands(Binder binder) {
return MapBinder.newMapBinder(binder, String.class, Command.class);
}
@Override
public void configure(Binder binder) {
// we don't have any commands in this module,
// but still need to call 'contribute*' once to ensure
// that an empty map is always available for injection.
contributeCommands(binder);
}
@Provides
@Singleton
CommandExecutor provideCommandExecutor(Map<String, Command> commandMap) {
return name -> commandMap.get(name).exec();
}
}
public static class AppModule1 implements Module {
@Override
public void configure(Binder binder) {
ReusableModule.contributeCommands(binder)
.addBinding("a")
.to(CommandA.class);
}
}
public static class AppModule2 implements Module {
@Override
public void configure(Binder binder) {
ReusableModule.contributeCommands(binder)
.addBinding("b")
.to(CommandB.class);
}
}
Here, we replaced a hardcoded map with an injectable map that is initially empty. Also we created a static contributeCommands
method that gives other modules a hint on how our reusable module can be extended. So downstream modules can add any number of commands that will all be available to the application.
This all sounds simple, but it is a very important design pattern. It turns our DI environment into a powerful modularity engine. Common algorithms can be "bound" in generic reusable modules, but the actual set of objects they operate upon is composed by all collaborating modules present in the app. "contribute*" methods comprise custom Module-level APIs, often the only thing we need to know about any given Module.
Conclusion
This concludes Part 1 of my Guice stories. It is not a substitute for the Guice documentation, but it will hopefully answer some practical questions and will get you over the learning curve faster. More stories will follow shortly.
Have something to say? Leave a comment or follow me on Twitter.
Published at DZone with permission of Andrus Adamchik, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments