Java Dependency Injection – Passing a Context with Dependency Injection

dependency-injectiongoogle-guicejava

In the project I'm working on, I'm using Guice and trying to do as much as possible with Dependency Injection. However, there's one little snag; many of my objects rely on an object Context. This is an immutable object but has some information which most of the other objects need to function properly. It never changes for classes that are all working within the same Context, but my application must be able to support different Contexts at the same time, in the same scope. I can think of three ways to solve my problem:

  1. Currently, I've found myself writing lots of code with AssistedInject, which almost always has methods like Factory.create(Context c). All my code that depends on Context is being created in this manner, even if they are otherwise simple.

  2. I'm starting to consider using Providers, and changing every line of factory.create(context) to obj = provider.get(); obj.setContext(c); but I don't want to rely on my code to do both calls, I like that the compiler makes sure that every object has it's context and that the context cannot change (it never will).

  3. My research pointed me to this thread: https://stackoverflow.com/questions/16316719/guice-assisted-injection-deeper-down-the-dependency-hierarchy The idea of pairing each Context to a childInjector which has one line bind(Context.class).toInstance(ContextFactory.makeInstance(args)), but I'm not sure how to ensure that everything is using the correct child injector. This seems like it might be best, but other than that thread (which isn't my scenario) I'm not sure how to do it, or if it's right for my situation.

Here's an sscce of what might be done by option 3:

public class GuiceChildTest {
    public static void main(String... args) {
        Injector inj = Guice.createInjector(new ParentModule());
        Injector fooChildOne = inj.createChildInjector(new FooModule(new Foo(10)));
        Injector fooChildTwo = inj.createChildInjector(new FooModule(new Foo(20)));

        Bar bar = fooChildOne.getInstance(Bar.class);
        System.out.println(bar.baz());

        bar = fooChildTwo.getInstance(Bar.class);
        System.out.println(bar.baz());
    }

    private static class ParentModule extends AbstractModule {
        @Override
        protected void configure() {
        }
    }

    private static class Bar {
        private MyInterface intf;
        @Inject
        public Bar(MyInterface in) {
            this.intf = in;
        }

        public int baz() {
            return intf.doSomething();
        }
    }

    private static class FooModule extends AbstractModule {
        private Foo f;
        public FooModule(Foo f) {
            this.f = f;
        }

        @Override
        protected void configure() {
            bind(MyInterface.class).toInstance(f);
        }   
    }   

    private static class Foo implements MyInterface {
        private int i;
        public Foo(int i) {
            this.i = i;
        }
        @Override
        public int doSomething() {
            return i;
        }
    }

    private static interface MyInterface {
        int doSomething();
    }
}

Best Answer

Let's address the fundamentals of Dependency Injection with a brief review of the dependency injection principle

When following this principle, the conventional dependency relationships established from high-level, policy-setting modules to low-level, dependency modules are inverted (i.e. reversed), thus rendering high-level modules independent of the low-level module implementation details. The principle states

  • High-level modules should not depend on low-level modules. Both should depend on abstractions.
  • Abstractions should not depend on details. Details should depend on abstractions.

It appears that the first two approaches you mentioned may not be fully following DI principles because the higher order objects appear to need to know what type of Context they should have.

And before I continue, I should admit a bias that will potentially offend fanatical DI proponents:

DI can get over-emphasized and zealous adherence to the principles can get in the way of writing maintainable code that's appropriate for the environment that it operates within. And that's not necessarily a dig against DI, but rather against any technique that ends up being blindly applied. I'm not saying that you are blindly applying DI, but there are many who fall into that category.

Spoiler used to protect those who are overly sensitive to such claims

So before calling methods 1 & 2 "bad", we need to ask if they are meeting your current needs. Those approaches involve some measure of boilerplate code that's copied around. This can be okay if you don't find yourself needing to change that template on a frequent basis.

Likewise, if the objects can use one and only one type of Context, then you may not need to worry about DI in this case as there isn't necessarily something else you'd inject into the object. "But what about testing?!" is the usual retort to that pragmatic claim, which is answered with an equally pragmatic answer of "Can those objects use / do they need a different Context for testing?" If yes, then use DI. If not, then ... carry on.

The 3rd option you propose gets you further away from the boilerplate code and gets you closer to a DI approach. Your hesitation appears to be:

but I'm not sure how to ensure that everything is using the correct child injector

I think the solution to that challenge is to wrap your Context creation within a Factory and absolve the higher order objects from knowing what they need to call.

So if I understood your code sample correctly, instead of this:

Bar bar = fooChildOne.getInstance(Bar.class);

you would have something like:

Bar bar = new Bar(ContextFactory.getInstance(Bar.class));

In the suggestion, we're passing the Context returned from the ContextFactory into Bar's constructor. It may be 1 trivial to simplify that example with:

public class Bar(){
    Bar(){
        return Bar(ContextFactory.getInstance(Bar.class));
    }
}

or:

public class Bar(){
    Bar(){
        this.Context = ContextFactory.getInstance(Bar.class);
    }
}

1 My Java is rusty, so I don't know if you can chain constructors like that first example.

And you could potentially tighten things up a bit by using reflection within the ContextFactory to determine the correct Context for Bar or optionally pass in additional parameters to the ContextFactory if Bar can handle multiple types of Context in regular operation.

TL;DR

Use the third option you proposed and wrap Context creation within a Factory.

Related Topic