Tuesday, 29 May 2007
Guice with Curry
I previously explained how
Guice works, but not how it changes your application's archiecture
when you use it. Now that I've had some practical experience, I can
explain a little more about that.
While refactoring my 20% project to use Guice for creating objects, I
found myself adding factory classes where previously the code would
call constructors directly. This seemed odd because up until now,
I've generally thought that factories are overused and best avoided.
Later in this article I'll explain why this happens. But first, a
digression into functional programming languages...
Currying in Haskell
A nice feature of functional programming languages such as Haskell is
that they support curried functions. For example, let's define an
(admittedly trivial) function in Haskell to multiply two integers:
product :: Integer -> Integer -> Integer
product a b = a * b
The first line declares the type of the product function.
(This looks a little weird and I'll get to that in a minute.) The
second line defines its implementation. Here is how you would try out
this function from a Haskell interpreter's command line:
> product 6 7
42
Haskell's syntax for functions looks rather strange to most
programmers: where are the parentheses? Actually, we could have put
parentheses around product's two parameters, but leaving them
out tells Haskell that the function is a curried function.
Curried functions have the property that you can call them with fewer
arguments than they're declared to take, and the result is another
function that takes the remaining arguments. Here are some examples:
double = product 2
> double 6
12
> double 7
14
triple = product 3
> triple 2
6
This trick works because a curried function such as product is
not actually a two-argument function. It's really a function that
takes one argument and returns a function that takes a second
argument, returning the answer. That explains the funny type
declaration:
Integer -> Integer -> Integer
For a non-curried function, the type declaration would look like this:
(Integer, Integer) -> Integer
Back to Java
So, that's a neat trick, but what does it have to do with Guice?
Suppose you are writing a Java class that can play a song. A first
attempt might look like this:
public class Song {
public Song(SongPlayer player, String name, File mp3) {
...
}
public void play() {
player.play(mp3);
}
}
This code seems perfectly reasonable when you're writing constructor
calls by hand. It's a little awkward to have to pass three arguments,
but the play method requires a player, so it seems like there's
no help for it. (Yes, there are other ways, like using static
variables, thread locals, or singletons, but they seem worse after
doing test-first development for a while.)
Let's say that in our application, the SongPlayer is configured
at startup and never changes, but each song has a different name and
mp3 file. So, we would like to Guice to inject SongPlayer while
leaving the other two arguments alone.
It might seem that a Provider is the answer to everything in
Guice, but in this case, the best option is to write a factory:
public class Song {
protected Song(SongPlayer player, String name, File mp3) {
...
}
public static class Factory {
private final SongPlayer player;
@Inject
public Factory(SongPlayer player) {
this.player = player;
}
public makeSong(String name, File mp3) {
return new Song(player, name, mp3);
}
}
}
Now we can inject Song.Factory into any class that needs to
create songs, and they will be able to do it without knowing anything
about SongPlayer.
I found myself applying this refactoring repeatedly while converting
my web app to Guice. Eventually it occurred to me that all I'm really
doing is currying the Song constructor!
That is, moving from this:
Song song = new Song(player, name, mp3);
To this:
Song song = new Song.Factory(player).makeSong(name, mp3);
Is analogous to removing the parentheses on a function in Haskell,
except that there is a lot more typing involved.
In general, you need to curry a class constructor whenever some of its
parameters are shared among all instances (and can be hidden) while
others contain instance-specific data (and need to be exposed). This
is a "code smell" that's largely invisible to programmers who haven't
done much dependency injection, but it's not hard to write the code
that way the first time, once you know what to look for.
How often you need a factory depends on your coding style. It's also
possible to use a service-oriented architecture for Song and
SongPlayer:
public class Song {
public Song(String name, File mp3) {
...
}
void play(SongPlayer player) {
player.play(mp3);
}
}
In this alternate design, any code that constructs songs can call the
Song constructor directly, but classes that want to play songs
need to be injected with a SongPlayer. As a result, no
currying is needed.
Each of these designs has its advantages. The service-oriented design
is admittedly more configurable, since each call to play could
use a different song player. If you have lots of songs, it might
also save memory because Song instances are smaller.
However, I prefer writing smart objects that have high-level methods
such as play that take few or no arguments. Songs that play
themselves might seem like a strange way to model the world, but I
find that reducing the number of arguments to a method tends to make
it easier to use.
Can we do better?
Apparently, the price for this when using Guice is that you have to
write lots of factories. But these factories are simply boilerplate.
It would be nice if a future version of Guice would just write them
for us. Here's a sketch of how Song might look when written
using some future version of Guice:
public class Song {
@Curried
protected Song(@Inject SongPlayer player, String name, File mp3) {
...
}
}
public interface SongFactory {
Song makeSong(String name, File mp3);
}
public class MyModule extends AbstractModule {
public void configure() {
factory(SongFactory.class).builds(Song.class);
}
}
In the meantime, writing factories yourself is still pretty clean and
the overall results of adopting Guice are worth it. I had thought my
code was well-organized before, but adopting Guice made it
singificantly cleaner. I expect that Guice will become one of the
libraries that I inevitably add to any web application I write, along
with Jetty, JUnit and EasyMock.
[Update as of July 2007: since this article was written, a couple of folks at Google wrote the AssistedInject extension to do exactly this, though with a different syntax. Also, there are plans to add it to Guice; see Issue 131 for updates.]
respond | link |
/code
|