2012年5月6日星期日

Guice: Explicit vs. Implicit Bindings

Okay, let's talk about explicit vs. implicit bindings in Guice.

I believe a lot of backslash from Guice users aren't necessarily Guice's fundamental flaw, but are due to the practice they were encouraged to try and then fail.

Before I start, I want to clear up a common mis-conception. It's probably due to this misconception that explicit binding (with Guice's bind().to() DSL) is accepted by many as best practice. Do you believe implicit bindings (just @Inject the class you need without Guice Module) don't report errors up-front at Injector start-up time?

If so, you are misinformed. Look at the following code example:

class Foo {
  @Inject Foo(Bar bar) {}
}
class Bar {
  @Inject Bar(Baz baz) {}
}
class Baz {
  @Inject Baz(Doh doh) {}
}
class ApplicationModule extends AbstractModule {
  @Override protected void configure() {
    bind(Foo.class);
  }
}

Injector injector = Guice.createInjector(new ApplicationModule());

With Foo as the root application-level binding, Guice already has full knowledge of Bar, Baz and Doh dependencies through reflection. It will report errors the same way as if you wrote explicit bindings for Bar and Baz. There is no need to manually "tell" Guice about them.

So what's my point? Avoid explicit bindings, avoid Guice Module's as long as you can.

But why? Doesn't it contradict AbstractModule.requireExplicitBindings()?

Explicit bindings and modules have the following inherent problems:

  1. They are not IDE-friendly. I'm reading Foo and need to navigate to Bar then Baz and maybe Doh to see how the system works. It's a 1-second no-brainer the way it is as above. With presence of modules and potentially providers, this one tiny step can become a multi-minute puzzle-solving. Am I exaggerating? Not at all. Not long ago in a complex web application I had to read, searching for a certain user's Locale binding took me half hour and still got it wrong!
  2. They turn compilation error into runtime Guice binding errors. If Foo, Bar and Baz in the above examples are all just classes with @Inject on their constructor, the code compiling means Guice will just work.
  3. They are hard to manage. Exactly which binding goes into which module and which bindings belong to the same module is a subtle art to prefect. If you are writing an integration test for Foo, or trying to reuse Foo as a library, expect going through a painful process of sifting Guice error stack traces, identifying all the modules you have to install through trial-and-error. Also expect having to pull in bindings you have no reason to need, but only because they are "neighbors" of bindings you do need. It's easier to create a mess than you might think.
  4. Guice bindings can grow stale as the program grow. What you needed yesterday as a transitive dependency may not be needed tomorrow and yet it's easy to forget to update your bindings, because no tool reminds you that. So if you rely on Guice modules as your "wiring document", beware that it's a stale document.
  5. They are boilerplate with low signal-to-noise ratio. Wiring should be done in the least intrusive way instead of getting in the way.
Main supposed benefits I've heard about preferring explicit bindings:
  1. "They give up-front error reporting." Again, this is simply irrelevant because implicit bindings do the same.
  2. "They show the centralized full picture of my wirings". Personally, I have yet to appreciate this as a useful thing. I read the actual classes to understand business logic, and "wiring" is only meaningful when I navigate between collaborators. I never ever needed to view "wiring" as a separate data outside of context of business logic. Even if it is indeed useful, since this wiring picture can be stale, it should really be generated by a tool, a tool that can even show us prettier lines and boxes and allows us to adjust scale or pick the sub-system we are interested in, just like how we view things in Google Map.
  3. "If you have both implicit binding and explicit binding for the same type, the implicit binding is a lie and can give reader false information." True, but is it a reason to discourage implicit bindings, or to discourage explicit bindings? I've long held the belief that Guice should detect and report errors when a class both has @Inject on a constructor, and is explicitly "bound" in a Module (well, except in Modules.override()). It's just another way to shoot yourself in the foot.
And yes. When you do end up creating an interface and an implementation class, a binding and a module are must-haves. But it shouldn't be the default practice. We create interfaces, add explicit bindings only when there is an explicit need (like, you have more than 1 implementations). Don't create them just as a matter of habit. What happens to the KISS principal?

Yes again: JIT binding on public no-arg constructors without @Inject is a mis-feature. Don't use it!

2012年4月19日星期四

Crazy Tricks To Play With TypeToken

Guava 12 will include a TypeToken class, that is designed to be the Class<T> for generic types such as Map<Integer, String>.

A few crazy tricks worth mentioning:

1)
When writing a framework or library, it's sometimes useful to get the java.lang.reflect.Parameterized with the type parameters provided at runtime, similar to Guice's Types.newParameterizedType().

Implementing your own ParameterizedType class has a lot of gotchas to deal with (you'll implement equals(), hashCode(), make sure it inter-operates with the built-in Type). At the call site it's also possible to screw up by passing in type parameters that violate the parameterized type's bounds: newParameterizedType(Enum.class, String).

TypeToken.where() provides a type safe yet flexible API to get at any Type, simple or complex:

static <K, V> TypeToken<Map<K, V>> mapType(
    Class<K> keyType, Class<V> valueType) {
  return new TypeToken<Map<K, V>>() {}
      .where(new TypeParameter<K>() {}, keyType)
      .where(new TypeParameter<V>() {}, valueType);
}

Read this as "Give me type of Map<K, V> where K=keyType and V = valueType". Or, if  you speak Haskell:

Map<K, V>
   where
     K = keyType
     V = valueType;

And this method gives an array type:

static <T> TypeToken<T[]> arrayType(TypeToken<T> componentType) {
  return new TypeToken<T[]>() {}
      .where(new TypeParameter<T>() {}, componentType);
}


We get compiler to work for us to guard against wrong number of type parameters, or type parameters that violate bounds: there is no way to create a TypeToken<Enum<String>>.

2)
Again, if you write crazy reflection frameworks, you may sometimes want to know what a method's return type *really* is.

I mean, List's get(int) method returns Object, and we can tell that by calling List.class.getMethod("get", int.class).getReturnType(). We also know that getGenericReturnType() returns <E>, the type parameter of class List.

But how do we tell that List<String>'s get(int) method returns String, through reflection?

resolveType() is designed for this purpose:

Method getMethod = List.class.getMethod("get", int.class);
new TypeToken<List<String>>() {}.resolveType(getMethod.getGenericReturnType());
    => String

3)
How do you tell if objects returned by method give() can be passed to method take() --- assuming take() accepts one parameter only?

If we disregard generics, it would be as simple (yuch) as:

take.getParameterTypes()[0]
    .isAssignableFrom(give.getReturnType())

But tough, let's consider generics. What if give() and take() are declared as the following?

<K> ImmutableMap<K, Integer> give(K key);
void take(Map<?, ? extends Number&Comparable> m);

JDK provides no API to do this.

TypeToken#isAssignableFrom() works almost like Class#isAssignableFrom(), except it also works for parameterized types, generic array types, wildcard types, and type variables:

TypeToken.of(take.getGenericParameterTypes()[0])
    .isAssignableFrom(give.getGenericReturnType());
  => true

And if instead take() is declared as the following:
void take(Map<Integer, String> m);

The result will be false.

So have at it. Our daily Java life shouldn't be troubled with reflection craziness like these. But they can be helpful in the unfortunate dark hours. :)