Wednesday, 23 Jan 2008
Inject is the new import
import com.example.Thing;
...
Thing.go();
|
import com.example.Thing;
...
@Inject
private Thing thing;
...
thing.go()
|
I've written before about how Guice works,
but didn't explain why you would want to use it. Here's one way to think about it.
I think of an import as a way of wishing for a class: "I wish I had a TurkeySandwich."
Poof! It magically appears. "Thanks, classloader!" You only need to know a class's
true name to summon it, at least as far as source code is concerned. As a result, adding a new
dependency from one class to another is trivial in Java. (Maybe too trivial, given some of the
spaghetti code out there.)
Now, instead of wishing for a class, let's say we want an instance of a class. If you're deep
down in the guts of large system, doing this properly can be significantly more work. We can add a
new field, but then we have to initialize it in a constructor or setter, and the data that goes into
the field could be far away. Sometimes this means modifying hundreds of constructor calls. A
refactoring IDE makes it easier, but it's still a much bigger change than adding an import
statement.
Naturally we want to find an easier solution. Constants, static method calls, and thread locals
have their place, but such shortcuts can really mess up the code base. Test-first programming can
help keep us on the straight and narrow, but it doesn't reduce the pain of refactoring to get data
for a new dependency to the right place.
Suppose that instead we made instances easy to wish for? That's where Guice comes in. Stripped
to its essentials, Guice lets you wish for an instance of a class by putting an @Inject
annotation on a field. Your class simply assumes that the environment will initialize the instance
correctly; exactly how this happens is none of your concern, just like you don't need to know
anything about classpaths or classloaders to write an import statement.
(Guice also has constructor and method injection, but I think of them as a way to fit Guice into
legacy systems. They're not necessary unless there is some code somewhere that doesn't use Guice
to create instances. I use Guice injection even when writing tests.)
Of course, it's not really that simple. For the genie to be able to grant our wishes, we have to
set up the environment. For import statements, that means setting up a classpath, or writing class
loaders in the more complex cases. For Guice, we set up the environment by writing annotations for
simple cases and writing Guice modules for more complex ones. Luckily, writing a Guice module is
much easier than writing a class loader.
Assuming the environment is set up, how do you structure your code differently when using
Guice?
I find that a lot of initialization clutter goes away. Classes don't need five different
constructors that take slightly different arguments; in fact, they sometimes don't need any
constructors at all. We can also get rid of static methods that essentially act as constructors,
and don't need to write getInstance() methods for singletons.
Also, when you get rid of "convenience" constructors, you may find that each class has fewer
imports, and those are only to the class's nearest neighbors in the dependency graph, making the
that much more modular and easier to change.
However, in their place, you can expect to see more factories. Guice only allows you to wish for
instances by class name and annotations; you can't pass any runtime parameters. Instead, you can
wish for a factory and call a method on the factory. (This is admittedly a different kind of
boilerplate that I hope will go away in a future version of Guice. For some workarounds for simple
cases, see Guice with Curry and
AssistedInject.)
A harder case is what we usually call the "robot legs" problem. Say I'm the AnkleBone class, and I
know that, according to the song, I need to be connected to the KneeBone, so I wish for a KneeBone
and Guice can't do it because it doesn't know which KneeBone to return. The AnkleBone may
be generic, so it doesn't know either. Guice is missing a way to create multiple, custom instances
of large graphs of objects and join them together.
Until this is fixed, we'll have to use factories that call other factories and pass parameters the
old-fashioned way. I have a few ideas on how to extend Guice, but that will have to wait for another
time. For now, Guice 1.0 goes a long way towards cleaner code.
respond | link |
/code
|