You're thinking about dependency injection incorrectly. Dependency Injection and Service Locator are mirror-images of each other: with a service locator, you ask it for an object. With dependency injection, you don't go looking for dependencies, they're just handed to you.
Basically, "it's turtles all the way down"! Every dependency your class has should be injected. If MyApplication
needs a MyConfiguration
object, it should just accept a MyConfiguration
object as a constructor parameter, and not worry about how it was constructed.
Now, this isn't to say that you can never use new
manually -- but you should reserve that for value-type objects that don't have external dependencies. (And in those cases, I'd argue that you're often better off with a static factory method than a public constructor anyway, but that's beside the point.)
Now there are a couple of ways of doing this. One way is to shard MyConfiguration
into lots of tiny pieces, so that instead of doing myConfiguration.getConfig("x")
you would do @Inject @Configuration("x") String
or something like that. Alternatively, you could make MyConfiguration
itself injectable, and then provide accessor methods on it for the pieces. The right answer depends somewhat on the kind of data you're trying to model -- make the dependencies too fine-grained and your bindings may become hard to maintain (although there are ways to make that better); make the dependencies too coarse and you make it harder to test (for example: which is easier, providing just the "x" config that the class you're testing needs, or building the whole application's config?).
You can even do both:
/** Annotates a configuration value. */
@BindingAnnotation
@Retention(RetentionPolicy.RUNTIME)
public @interface Config {
String value();
}
/** Installs bindings for {@link MyConfiguration}. */
final class MyConfigurationModule extends AbstractModule {
@Override protected void configure() {}
@Provides
@Singleton
MyConfiguration provideMyConfiguration() {
// read MyConfiguration from disk or somewhere
}
@Provides
@Config("x")
String provideX(MyConfiguration config) {
return config.getConfig("x").getName();
}
}
// elsewhere:
/** The main application. */
final class MyApplication {
private final String xConfig;
@Inject MyApplication(@Config("x") String xConfig) {
this.xConfig = xConfig;
}
// ...
}
You can take a similar approach in unit tests:
/** Tests for {@link MyApplication}. */
@RunWith(JUnit4.class)
public final class MyApplicationTest {
// Note that we don't need to construct a full MyConfiguration object here
// since we're providing our own binding, not using MyConfigurationModule.
// Instead, we just bind the pieces that we need for this test.
@Bind @Config("x") String xConfig = "x-configuration-for-test";
@Before public void setUp() {
// See https://github.com/google/guice/wiki/BoundFields
Guice.createInjector(BoundFieldModule.of(this)).injectMembers(this);
}
@Inject MyApplication app;
@Test public void testMyApp() {
// test app here
}
}
Dependency injection also encourages another best practice which I highly recommend, which is to design your type system such that invalid states are not representable (to the maximal degree possible). If all the configuration MyApplication
needs is passed in its constructor, it's impossible to ever have a MyApplication
object that doesn't have a valid configuration. This allows you to "front-load" your class invariants, which makes it much easier to reason about the behavior of your objects.
Finally, a note about Injector.getInstance()
. Ideally you use Injector
exactly once in your program: immediately after it is constructed. That is, you should be able to do Guice.createInjector(...).getInstance(MyApplication.class).start()
and never store a reference to the Injector
anywhere. I tend to build applications using Guava's ServiceManager
abstraction (see also this question), so the only thing I ever need to do is:
public static void main(String[] args) throws Exception {
Injector injector = Guice.createInjector(...);
ServiceManager manager = injector.getInstance(ServiceManager.class);
manager.startAsync().awaitHealthy();
}