chandra_cst
chandra_cst

Reputation: 307

Bindings in Guice: Compile time dependencies

While exploring Guice, I had a question on the way the dependencies are injected.

Based on my understanding, one of the important aspects of DI is that, the dependency is known and is injected at runtime.

In Guice, to inject a dependency we either need to add the binding or implement a provider. Adding a dependency takes a class object which adds a compile time dependency on that class. One way to avoid that is to implement it as a provider and let the provider use reflection to dynamic load the class.

public class BillingModule extends AbstractModule {

@Override
protected void configure() {
    bind(CreditCardProcessor.class).toProvider(
            BofACreditCardProcessorProvider.class);
    bind(CreditCardProcessor.class).annotatedWith(BofA.class).toProvider(
            BofACreditCardProcessorProvider.class);
    bind(CreditCardProcessor.class).annotatedWith(Amex.class).toProvider(
            AmexCreditCardProcessorProvider.class);
}

@Provides
PaymentProcessor createPaymentProcessor() {
    return new PayPalPaymentProcessor();
}

@Provides
PayPalPaymentProcessor createPayPalPaymentProcessor() {
    return new PayPalPaymentProcessor();
}}

Is there a reason why Guice choose class object over class name? That could have removed the compile time dependency right?

Upvotes: 3

Views: 2954

Answers (1)

Daniel Pryden
Daniel Pryden

Reputation: 60997

If your interface and implementation are defined in the same dependency (that is, in the same JAR file) then you already have a hard build dependency on the implementation, whether you use Guice or not.

Basically, as soon as you have:

public final class MyClass {
  public void doSomething(Foo foo);
}

Then to compile MyClass a definition of Foo needs to be on the compile-time classpath.

The way to resolve this is to separate out the interface from the implementation. For example, if Foo is an interface, and FooImpl is the implementation of it, you would put FooImpl in a different dependency (that is, a different JAR file) from Foo.

Now, let's say you have two sub-projects in Maven:

foo-api/
  pom.xml
  src/main/java/com/foo/Foo.java

foo-impl/
  pom.xml
  src/main/java/com/foo/FooImpl.java

Where should the Guice module that binds Foo live? It shouldn't live in the foo-api project, it should live in the foo-impl project, alongside FooImpl.

Now suppose you have a separate implementation of Foo (let's call it SuperFoo), and your project needs a Foo, but it could be either FooImpl or SuperFoo.

If we make SuperFoo its own project:

super-foo/
  pom.xml
  src/main/java/com/super/foo/SuperFoo.java
  src/main/java/com/super/foo/SuperFooModule.java

Now all your application code can simply @Inject Foo and use the foo. In your main() method (or wherever you create your Injector) you need to decide whether to install FooModule (from foo-impl) or SuperFooModule (from super-foo).

That is the place where reflection may be warranted. For example, you could have a configuration flag foo_module which could be set to either "com.foo.FooModule" or "com.super.foo.SuperFooModule". You could decide which one to install using code like this:

public static void main(String[] args) {
  Config config = parseConfig(args);
  List<Module> modules = new ArrayList<>();
  modules.add(...); // application modules

  String fooModuleName = config.get("foo_module");
  Class<? extends Module> moduleClass =
      Class.forName(fooModuleName).asSubclass(Module.class);
  modules.add(moduleClass.newInstance());

  Injector injector = Guice.createInjector(modules);
  injector.getInstance(MyApplication.class).run();
}

Of course, you could also use any other mechanism you like to select which module to install. In many cases, you don't even really want to do this reflectively, you can simply change the code at the same time you change the build dependency.

Upvotes: 3

Related Questions