Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Instantiating per-scope/group singletons - stuck in Guice dependency hell

This question is a continuation of Using guice for a framework with injected classes, proper way to initialize?, which I've tried to implement, and also tried other ways to get around the issue, but nothing has worked so far.

The main issue is this. I have an InterfaceA and InterfaceB that are exposed in different parts of the API. There are two classes that implement both of these interfaces, TestClass and RealClass, so that depending on whether I'm testing or doing something else, I can do the following:

bind(InterfaceA.class).to(TestClass.class);
bind(InterfaceB.class).to(TestClass.class); 

or, for production:

bind(InterfaceA.class).to(RealClass.class);
bind(InterfaceB.class).to(RealClass.class);

I have two requirements for using these classes:

  1. I need the same instance of TestClass or RealClass to be bound to all injections of InterfaceA and InterfaceB; so, like a singleton pattern, except that:
  2. The singleton is only for a specific scope or child injector, many of which are created during the execution of the program.

The default no-scope approach causes multiple instances of RealClass/TestClass to be created for each interface injection. I don't want that, so I've tried implementing this with scopes, child injectors, and other methods. Nothing has worked:

  • Child injector approach: I create a new injector and try to bind the TestClass or RealClass to a singleton instance in that injector. The problem is, whether TestClass or RealClass is being used is configured in the parent injector, and since it's a singleton, it's already instantiated (unless in Stage.DEVELOPMENT). There's no way to bind InterfaceA to TestClass, in the parent injector, for example, and then re-bind it as a singleton in the child injector.
  • Scope approach: I create a custom scope and annotate TestClass and RealClass. Then, I enter and exit this scope to get single instances in that scope. The problem is that my code is multithreaded and having the scope change from one thread affects what the global injector can see and mucks up creating other instances.
  • Combined child injector and scope approach. I tried creating a child injector for each use of this custom scope, but then binding RealClass in the parent fails with

    No scope is bound to name.package.WhateverScope.
    

    because it seems to insist that the WhateverScope is available all the time, not just in the child injector.

All these problems seem to be due to the fact that I need to be able to configure whether to use TestClass or RealClass in the parent, but then to be able to instantiate them later, as a singleton, for a specific group of objects. I'm pulling my hair out over how to get this done!

By the way, the documentation for Guice scopes is horrible and almost impossible to understand. This article is the only one that has gotten me anywhere:

like image 433
Andrew Mao Avatar asked Feb 26 '13 23:02

Andrew Mao


1 Answers

Apologies for a somewhat-breakthrough less than an hour after posting.

I seem to have been able to fix this by somewhat abusing the thread-local scope implementation provided at http://code.google.com/p/google-guice/wiki/CustomScopes. It seems to be a somewhat clean way to solve this problem without using child injectors. I'm not sure if it's 'proper', though. I'll still accept other answers.

Here's what I did. First, I create one instance of the scope, bind it to the appropriate annotation, and make it available in the injector:

ThreadLocalScope scope = new ThreadLocalScope();
bindScope(ExperimentScoped.class, scope);
bind(ThreadLocalScope.class).toInstance(scope);

Then, as the documentation says, I need to bind a fake provider for every type of key that would be seeded in the scope:

bind(SomeKey.class)
  .toProvider(ThreadLocalScope.<SomeKey>seededKeyProvider())
  .in(ExperimentScoped.class);
bind(SomeOtherKey.class)
  .toProvider(ThreadLocalScope.<SomeOtherKey>seededKeyProvider())
  .in(ExperimentScoped.class);

I may also have some other scope-able objects that I want to be distinct within each scope, so I bind those too. These are the TestClass and RealClass above. There may be also SomeScopedClass that was annotated with the @ExperimentScoped:

bind(InterfaceA.class).to(TestClass.class).in(ExperimentScoped.class);
bind(InterfaceB.class).to(TestClass.class).in(ExperimentScoped.class);

bind(SomeInterface.class).to(SomeScopedClass.class);

Finally, I can use the scope to create distinct sets of interdependent objects, in parallel from different threads. Each thread can do something like the following, even though they are using the same injector:

ThreadLocalScope scope = injector.getInstance(ThreadLocalScope.class);      
scope.enter();

try {
    // Seed the seed-able keys
    scope.seed(SomeKey.class, keyInstance);
    scope.seed(SomeOtherKey.class, otherKeyInstance);    

    SomeScopedClass instance = injector.getInstance(SomeScopedClass.class);

    // Hooray! instance was injected with the seeds and created just for this scope!
}
finally {
    scope.exit(); // Throws away the scope and referenced objects.
}

In my case, I can discard the scope completely because I don't care about keeping track of the set of objects in the scope once they're wired up properly. But it probably wouldn't work if I wanted to come back to this scope later and inject some more objects.

Hope this helped someone. The Guice scoping documentation is terrible!

like image 98
Andrew Mao Avatar answered Sep 21 '22 19:09

Andrew Mao