Search code examples
javadependency-injectionguiceassisted-inject

Managing complex life cycles in Guice


I've run into a situation where I have a graph of data objects, and would like to create one service for each node on this graph. The problem is this service (and its dependencies) are dependent on the the node they're working for. Something like this:

class Visitor {

  void enter(Node node){
    Service service = guice.create(node)

    service.doComplexDomainLogic(/*some runtime information about the graph and the path to get here, other things*/)
  }
}

Note that my intention here is to create a new instance of Service, and a new instance of any dependencies for Service, for every node on the graph.

So now I have a couple options:

  • guice is backed by an assisted inject factory. This is what we're currently doing, and requires that the dependency on node is curried up through everything service depends on, if they too depend on node. In other words, if I use an assisted inject factory here, I have to use it for each class that service depends on, which is obnoxious.
  • I can use what we call nested bootstrappers, that is, a new module with new injector, effectively a new environment, and in the setup for that environment I can write bind(Node.class).toInstance(node). This code will be executed on the first visit, and have its result cached.
  • I can use guice's custom scopes (I think).

We've done the second in a couple places simply because me and my team weren't aware of custom scopes. We now have one other use of a custom scope, and I must confess it was much more complex to implement than I would like. Following this guide helps, but its clear that I'm going to have to delve back into thready-safety land, and my last visit there was not so pleasant.

What guice facility should I use to get this behaviour?


Solution

  • You can inject the Injector and create a child injector for the pieces you want. A child injector will let you amend the object graph and access all of the parent injector's dependencies, but you can access your Node as deep as you'd like.

    class Visitor {
      @Inject Injector injector;
    
      void enter(final Node node) {
        Service service = injector.createChildInjector(new AbstractModule() {
          @Override public void configure() {
            bind(Node.class).toInstance(node);
            // Anything that does require a Node should be bound here, because
            // you can't define it in the parent due to the unsatisfied binding.
            bind(SomeInterface.class).to(SomeClassThatRequiresNode.class);
          }
        }).getInstance(Service.class);
    
        service.doComplexDomainLogic(/* ... */)
      }
    }
    

    Though it's possible to do something similar with scopes, bear in mind that scopes are only meant to identify when to create a new object versus when to return the same object. This means that you could create a @NodeScoped scope and ensure that the same objects would be returned while processing a given node, but you would still need to bind some kind of @NodeScoped NodeHolder to hold your nodes as you dive in. Rather than keeping and populating this separate holder, a child injector will let you ask for a Node directly, which may make your code easier to understand and test.

    See also: