Sunday, 08 Apr 2007
Guice, the Automatic Puzzle Factory
Lately I've been puzzled and intrigued by a Java 5 library called Guice, which Google
released
as open source last month. Guice is what's known as a dependency
injection framework. It's sometimes compared to Spring, but that won't help
you if you haven't used dependency injection frameworks or Spring. I
think of Guice as a toolkit for building graphs of
interdependent objects, or to put it another way, it's an automatic
puzzle factory. Here's a short introduction to how Guice works.
|
|
It sometimes happens that you want to plug a bunch of Java objects
together into a tree or graph. For example, suppose you have
interfaces A, B, C, and D, which are implemented by classes AImpl,
BImpl, CImpl, and DImpl. (Guice doesn't require you to use
interfaces, but it makes the example easier.) To build a tree
without Guice, we create the nodes bottom-to-top starting with the
leaf nodes:
public static A makeA() {
B b = new BImpl(new DImpl());
C c = new CImpl(new DImpl());
return new AImpl(b, c);
}
Guice can create the same tree automatically. Once we set up a
factory using Guice, we only need to tell it what kind of root node we
want:
A a = abcdFactory.getInstance(A.class);
Furthermore, the same factory will build a tree starting with any
other root. It's like an automatic puzzle factory that, given a
starting piece and a box of pieces, will automatically add pieces
until it constructs a puzzle. (It stops when there are no more places
to attach pieces; there can be plenty of pieces left over.)
Before we can start building puzzles, we need to describe the pieces
to Guice. We do that by implementing a method that specifies some
constraints:
protected void configure() {
bind(A.class).to(AImpl.class);
bind(B.class).to(BImpl.class);
bind(C.class).to(CImpl.class);
bind(D.class).to(DImpl.class);
}
This specifies that there are four kinds of "slots" (A though D) into
which four pieces can fit (AImpl through DImpl). The implementation
classes are the four types of nodes from which we will build trees.
Next, Guice automatically figures out the shapes of the pieces: what
other slots do they have where additional pieces should be attached?
It does this by searching for annotations using reflection. Each
implementation class has either a no-arg constructor (in which case
there there are no more slots to fill, resulting in a leaf node), or
it has a constructor that's annotated with @Inject. In the tree,
there will be a child for each parameter.
Here is a Guice-eye view of the classes AImpl through DImpl. All
Guice cares about is that AImpl has two slots to fill, BImpl and CImpl
have one, and DImpl has zero.
class AImpl implements A {
@Inject AImpl(B b, C c) { ... }
...
}
class BImpl implements B {
@Inject BImpl(D d) { ... }
...
}
class CImpl implements C {
@Inject CImpl(D d) { ... }
...
}
class DImpl implements D {
DImpl() {}
...
}
As a result, if Guice is asked to build an instance of A, the result
is the graph in the picture at the top of this article. There are two
different slots where D appears, so Guice has to create DImpl twice.
From four possible types of node, it creates a tree with five nodes.
But Guice isn't limited to trees; it can build directed
acyclic graphs and graphs with cycles as well. Here's another
example where we build a graph with two cycles out of a pool of seven
kinds of nodes. The methods named "backlink" cause additional edges
to be created going upward, causing the cycles.
class AImpl implements A {
@Inject AImpl(B b, C c) { ... }
}
class BImpl implements B {
@Inject BImpl(F f, G g) { ... }
}
class CImpl implements C {
@Inject CImpl(D d) { ... }
@Inject void backlink(A a) { ... }
}
class DImpl implements D {
@Inject DImpl(E e) { ... }
}
class EImpl implements E {
@Inject EImpl(F f, G g) { ... }
@Inject void backlink(C c) { ... }
}
class FImpl implements F {
@Inject FImpl(G g) { ... }
}
class GImpl implements G {
@Inject GImpl() { ... }
}
|
|
(Guice doesn't care what you name your methods. It looks for all
methods with the @Inject annotation and adds their parameters as
additional slots to fill. Guice will call these methods when putting
together the graph, creating outgoing edges.)
Here's how we tell Guice about our pieces:
protected void configure() {
bind(A.class).to(AImpl.class);
bind(B.class).to(BImpl.class);
bind(C.class).to(CImpl.class);
bind(D.class).to(DImpl.class);
bind(E.class).to(EImpl.class);
bind(F.class).to(FImpl.class).in(Scopes.SINGLETON);
bind(G.class).to(GImpl.class);
}
Why is F different? By default, Guice assumes you're trying to build
a tree. There are two nodes with slots for F, so normally there would
be two instances of FImpl, each with its own instance of G.
But setting a scope overrides this. We tell Guice that FImpl is a
singleton, which means that it will create FImpl once and reuse it,
resulting in two nodes pointing at the same instance of FImpl. (This
is unnecessary for nodes that are part of a cycle. Rather than
creating a tree of infinite size, Guice automatically links back to an
ancestor node.)
Actually, it wasn't necessary to use backlink methods. If you define
all the edges in your graph using constructors, Guice can still
construct the graph! Since construction is from the bottom up, edges
in the graph going upward are actually implemented using invisible
proxy instances. The edge can be attached later, after its
destination node is constructed.
(I think I would still put in the backlink methods, because otherwise
it would be hard to connect nodes into cycles manually. You can avoid
cycles while testing using mocks, but I'd rather not require users of
my classes to always use Guice. Maybe there should be a mode where
Guice warns about this? The use of proxies also means you need to
careful not to violate encapsulation; a node can't cast an interface
to its implementation because the cast will fail when there's a proxy.
But this is a good idea anyway.)
There are also some interesting things you can do by hooking into the
proxies, like adding logging; Guice uses proxies to implement a simple
version of aspect-oriented programming.
There are a lot more features that you can read about in the User's
Guide, and probably more that are yet undocumented. Much of what
I learned above came from experimenting.
Knowing what Guice does doesn't tell you what it's actually good for,
and what sort of program architecture you'll end up with if you use it
in a real application. I'm still figuring that out and I'll write
about it later.
[Update on May 26, 2007: the original version of this article implied
that Guice works better if you bind interfaces to classes. That's not
the case; both the type being bound and the implementation can be
classes, and no subclass is necessary.]
respond | link |
/code
|