Thursday, December 14, 2017

Partial Dependency Injection with Guice

Little Guicers

I hate Guice. I hate all the DI frameworks, in fact. It's a lot of magic for very little benefit. Java 8 and beyond is perfectly capable of letting you model your system plumbing in pure Java using a pattern like Cake for Scala, or simply using (gasp) the new keyword in a straight-up wiring method that looks a lot like Spring Java config, only more understandable and IDE/debug friendly.

But never mind that. I was recently called upon to do something like "partial" dependency injection using Guice, because I had the unique desire to

  • produce multiple instances of a class C
  • which had a constructor that required several collaborators
  • some of which were known at compile time, and some of which would vary for each instance
  • and these instances needed to be AOP-Alliance-enabled proxies, so that interceptors would work on them

That last bit ruled out writing my own factories by hand which used new, since they would never have the necessary enhancement by Guice.

Currying

The Google-verse sort of answered the question by mentioning FactoryModuleBuilder. What the hell is that? Well, the easiest way for me to think about it is in terms of currying in functional languages.

> val f = ( x : int, y : int, z : int ) => x * y + z
f : int * int * int => int

> val curryF = ( x : int ) => ( y : int ) => ( z : int ) => f( x, y, z )
curryF : int => int => int => int

> val someF = curryF( 2 )
someF : int => int => int

> val moreF = someF( 3 )
moreF : int => int

> val answer = moreF( 4 )
answer : int = 10

Factories in Guice

Factories in OO languages are basically curried, partially evaluated constructors. (Builders are like extreme versions of factories where the parameters are named.) In normal instantiation you do new Thing( x, y, z ) and with a factory you do new ThingFactory( x ).make( y, z ). My requirements above meant that I needed Guice to give me a little Guice factory for instances which was like a partially-evaluated injector: I knew x at compile-time and had bound it up with Guice already, but y and z would be unique to each Thing.

The code

Binding a static Thing would look like this:

public class Thing {
    @Inject
    public Thing( X x, Y y, Z z ) { /* ... */ }
}

public class ThingModule extends AbstractModule {
    @Override
    public void configure() {
        bind( X.class ).toInstance( x );
        bind( Y.class ).toInstance( y );
        bind( Z.class ).toInstance( z );
        bind( Thing.class );
    }
}

// Guice-bound Thing 
Guice.createInjector( new ThingModule() ).getInstance( Thing.class );

The factory way looks like this (notice the @Assisted annotations):

public interface ThingFactory {
    Thing make( Y y, Z z );
}
public class Thing {
    @Inject
    public Thing( X x, @Assisted Y y, @Assisted Z z ) { /* ... */ }
}

public class ThingModule extends AbstractModule {
    @Override
    public void configure() {
        bind( X.class ).toInstance( x );
        install( new FactoryModuleBuilder().build( ThingFactory.class ) );
    }
}

// Guice-bound ThingFactory creates parameterized, yet Guice-bound, Thing
// I guess we're "assisting" Guice by providing y and z on our own, hence the name
Guice.createInjector( new ThingModule() ).getInstance( ThingFactory.class ).make( y, z );

Of course the main way to use this construction is to inject the ThingFactory itself into some other object that needs dynamic Things.

The amazing thing is that if any method of Thing happens to match an interceptor, the resulting instance will be an AOP-alliance proxy and the interceptors will work. A hand-built factory using new, by contrast, would produce plain Thing instances that would be oblivious to interceptors.

Everything old is new again

This problem turns out to be very similar to something I posted about on this very blog almost a decade ago. In that case we needed a method of a Spring-decorated thing to produce another Spring-decorated thing, and used an interceptor on that method to wrap the result with a blindly delegating prototype bean produced by Spring. That technique probably works here too but takes more code.

No comments: