John Du
John Du

Reputation: 247

Provide instance on condition no other module provides that instance yet

My application uses Guice for Dependency Injection and consists of several modules, some depend on an instance of class X and some need to be able to run independently of the main application. So in the MainModule I have to provide an instance of class X, while some sub-modules also need to provide that instance, since their respective applications need to be able to run without the MainModule providing said instance of class X. Which leads to errors because "an instance of class X was already bound".

I have been looking around for a while now, but mostly I find references to PrivateModules which don't really do what I need, also I found a lot on OptionalBindings which, as far as I understand, mainly provide default values.

What I need is some sort of conditional binding as in "If another module provides an instance of class X do nothing, if no other module provides an instance of class X provide this one."

Upvotes: 8

Views: 817

Answers (3)

iluxa
iluxa

Reputation: 6969

Isolation

The problem is caused by insufficient isolation of various sub-systems. From a sub-system S point of view, type X has changed its contract over time. In the past, the type gave S ability to control all of its instances. But with integrations, control of instances was lost. Type X changed its behavior in an incompatible way.

Sub-system S should have been more isolated. It should not have used types that are potentially in conflict with other systems. It should have used its own private types instead, there'd be no issue then.

Now let's say in our hypothetical problem, there is a Database sub-system. It uses timeout when issuing network calls. It wants an instance of Timeout that has the value of 100 millis. EmailSender sub-system expects to be slower. Its Timeout has to equal 5 seconds. The systems conflict when integrated.

One approach: private wrapper types

    // EmailSender-private wrapper. Class is not public on purpose.
    class EmailSenderTimeout {
      final Timeout timeout;
      EmailSenderTimeout(Timeout t) { this.timeout = t; }
    }

    // In the module
    bind(EmailSenderTimeout.class)
        .toInstance(new EmailSenderTimeout(Timeout.seconds(5));

    // In the service
    @Inject
    EmailSendingService(EmailSenderTimeout timeout) {
      long millis = timeout.timeout.millis();
    }

Walla! Should anyone ever go and bind Timeout to whatever their heart desires, we the EmailSender still have our 5 seconds!

We achieved through isolation. We are still sharing the Timeout type, but we are no longer sharing instances.

Guicier: binding annotations

This mechanism is Guice's answer to our exact problem.

    // Define this annotation once in the sub-system somewhere.
    // Perhaps even directly in the Module class.
    @Retention(RetentionPolicy.RUNTIME)
    @BindingAnnotation
    @interface ForEmail { }

    // EmailModule
    protected void configure() {
      bind(Timeout.class).annotatedWith(ForEmail.class)
          .toInstance(Timeout.seconds(5);
    }

    // Service
    class EmailSendingService {
      @Inject
      EmailServiceImpl(@ForEmail Timeout timeout) {
        long millis = timeout.millis();
      }
    }

You can reuse the annotation for other shared types, too:

    class EmailServiceImpl {
      @Inject
      EmailServiceImpl(@ForEmail Timeout timeout, 
          @ForEmail RemoteAddress remoteAddress, 
          @ForEmail Protocol protocol) {
      }
    }

Each sub-system would declare its own private binding annotation and use it throughout.

In absolute, no two sub-systems should bind the same types, whether or not they are integrated today.

Simplistic mental model of Guice

There must never be duplicates in bindings:

    class Guice {
      HashMap<Key, Provider> bindings;
    }

    // Combines 3 things: Class, Generic Types, and Annotation
    class Key {
      Class<?> actualClass;
      @Nullable Class<?> annotationClass;
      @Nullable Type genericTypes;
    }

More details: Key.java, TypeLiteral.java

Upvotes: 1

joshng
joshng

Reputation: 1540

I think you can use Module-equality to address this need:

Guice adds each module that you install into a Set, which has the effect of deduplicating any Modules that are redundantly installed, as long as they considered equal.

So, you can embed the binding of class X in its own Module, and ensure that its module-class has an equals method which will identify other instances of the same module as equal.

For example:

class XModule extends AbstractModule {
  @Override
  protected void configure() {
    bind(X.class).to(RealX.class); // whatever you need to deduplicate
  }

  @Override
  public boolean equals(Object other) {
    return other == this || (other instanceof XModule);
  }

  @Override
  public int hashCode() {
    return XModule.class.hashCode();
  }
}

class ModuleA extends AbstractModule {
  @Override
  protected void configure() {
    install(new XModule());
  }
}

class ModuleB extends AbstractModule {
  @Override
  protected void configure() {
    install(new ModuleA());
    install(new XModule()); // will be deduplicated
  }
}

(this is also discussed here: Ensure Module Is Loaded Only Once In Guice)

Upvotes: 0

Duane
Duane

Reputation: 281

https://google.github.io/guice/api-docs/latest/javadoc/com/google/inject/util/Modules.html

The override methods are probably what you want

Upvotes: 1

Related Questions