Tapestry Training -- From The Source

Let me help you get your team up to speed in Tapestry ... fast. Visit howardlewisship.com for details on training, mentoring and support!

Tuesday, April 03, 2007

Tapestry 5 IoC from a Guice Perspective

So Guice has a few good ideas, and with Tapestry 5 still in a fluid state, I've absorbed a few of them into Tapestry 5 IoC. At the core, is the key ObjectProvider interface:

public interface ObjectProvider
{
    <T> T provide(Class<T> objectType, AnnotationProvider annotationProvider, ServiceLocator locator);
}

Implementations of this interface plug into a chain-of-command within Tapestry and, via the AnnotationProvider, can query for the presence of annotations to help decide exactly what is to be injected. A lot of Guice's examples, things like the @Blue service, are easily replicated in Tapestry 5 IoC, there's just not a lot of need for it. I supposed it would look something like:

public class BlueObjectProvider {
  private final Map<Class,Object> _blueObjects;

  public BlueObjectProvider(...) { ... }

    <T> T provide(Class<T> objectType, AnnotationProvider annotationProvider, ServiceLocator locator) {
      if (annotationProvider.getAnnotation(Blue.class) == null) return null;

      return objectType.cast(_blueObjects.get(objectType));
  }
}

Implicit here is a service configuration of implementations, keyed on object type, used when the @Blue annotation is present. Here's what the module would look like:

public class BlueModule {

  public ObjectProvider buildBlueProvider(Map<Class, Object> configuration)
  {
    return new BlueObjectProvider(configuration);
  }

  public void contributeMasterObjectProvider(
    @InjectService("BlueProvider") ObjectProvider blueProvider,
    OrderedConfiguration<ObjectProvider> configuration) {
    
    configuration.add("Blue", blueProvider);
  }

  public void contributeBlueProvider(MappedConfiguration<Class, Object> configuration,
    @InjectService("MyBlueService") Service myBlueService) {
    configuration.add(Service.class, myBlueService);
  }
}

public class OtherModule {

  public OtherService build(@Inject @Blue Service service) {
    return new OtherServiceImpl(service);
  }
}

On the one hand, this is more verbose, since its somewhat more procedurally based than Guice's approach, which is pattern based. On the other hand, it demonstrates a couple of key features of Tapestry 5 IoC:

  • Service construction occurs inside service builder methods; there's no layer of abstraction around service instantiation, configuration and initialization ... you just do it in Java code. There simply isn't a better language for descibing instantiating Java objects and invoking Java methods than Java itself.
  • Dependency injection is a concern of the module not the service implementation. The special annotations go in the module class, and the service implementation class is blissfully unaware of where those dependencies come from or are identified and obtained.
  • Dependencies are passed to the service builder methods, which do whatever is appropriate with those dependencies; in many cases, the dependencies are not passed to the instantiated class, but are used for other concerns, such as registering the new service for event notifications from some other service.
  • The contributeBlueProvider() method can appear in many different modules, and the results are cumulative. This allows one module to "lay the groundwork" and for other modules to "fill in the details". And, again, if this style of injection ("flavored" with the annotations) really became popular, it would be easy to integrate it into the central Tapestry 5 IoC module so it could be leveraged everywhere else.

In fact, in the OtherModule class, you can see how the injection lays out once the groundwork is prepared: Just an @Inject qualified with @Blue and you're done. Again, I can't emphasize enough how well moving construction concerns into the service builder methods works; it gives you complete control, it's unit testable in a couple of different ways, it keeps the service implementations themselves clean (of dependencies on the IoC container -- even in the form of annotations). It also keeps explicit what order operations occur in. HiveMind chased this with ever more complex XML markup to describe how to instantiate, configure, and initialize services.

That last feature, the interaction of multiple modules discovered at runtime, is the key distinguishing feature, and the one that's hardest to grasp. For me, it is the point where an IoC container transitions from an application container to a framework container. In an application container, you explicitly know what modules are present and how they will interact. It's your cake, starting from raw flour, sugar and eggs. Nothings in the mix that you didn't add yourself.

With HiveMind and Tapestry 5 IoC, the goal is to build off of an existing framework, and its interwoven set of services. The framework, or layers of framework, provide the cake, and you are trying to add just the icing. Configurations are the way to inject that icing without disturbing the whole cake, or even knowing the cake's recipe.

So Guice is a good kick in the pants, and Tapestry 5 IoC has evolved; the old @Inject annotation, which used to include a string attribute to name the thing to inject, has been simplified. The @Inject annotation is still there, but the string part is gone. Tapestry primarily works from the object type to match to the lone service implementing that interface. When that is insufficient, there's the MasterObjectProvider and Alias service configurations, which provide plenty of room to disambiguate (possibly by adding additional annotations to the field being injected).

The big advantage is type safety; refactor the name of a service interface and you'll see little or no disruption of the service network, because dependencies are almost always expressed in terms of just the (refactored) service interface, rather than any additional layer of "logical name".

I think there's a growing issue with Google-worship. It was interesting and disturbing to see so many people announce "Spring is dead! Long live Guice!" on various blogs (and I'll even admit to a little envy ... how come Tapestry 5 IoC doesn't generate this kind of interest?). Guice itself is pretty useless by itself, just as Spring's IoC container is, by itself, useless. The value of Spring is in what's built on top of the container: Wrappers for JDBC, Hibernate, JPA, JMS and every other acronym you can name. Transaction support and interceptors. And, more recently, the really solid AspectJ layer. If Guice wants to be a Spring-killer, or even a real player in the IoC space, it needs to embrace and extend Spring: make it easier to use those Spring integrations than it would be using Spring natively.

17 comments:

Jacob Hookom said...

With the CoR solution proposed, what about allowing a provider to assert the value is :null: without allowing propagation?

Unknown said...

This hasn't been an issue; I really prefer the "don't allow null, don't return null" approach, wherever possible. It's very easy to create a no-op implementation of a service (HiveMind can do it automatically, and that would be easy to port to T5).

Thiago H. de Paula Figueiredo said...


how come Tapestry 5 IoC doesn't generate this kind of interest?


Maybe because it is not advertised as a standalone library. Interestringly, I was just about to post a message in tapestry-users asking whether T5 IoC can be used outside of T5. Can it?
If yes, why don't you give it another name, making it clear that it is not just a part of T5?

Unknown said...

Because in T4, I constantly got bashed for NOT using Spring, even though Spring doesn't have the key feature, the configurations, that I need. So I thought I'd take a low profile on the IoC container in T5 ... which is why its amusing that Guice can fully embrace Not Invented Here and get accolades for it (from a blogging minority).

Thiago H. de Paula Figueiredo said...

It would be very, very interesting if there was a short, simple T5-IoC tutorial just like HiveMind's one. ;) What do you think about it? I'm curious to feel the difference between using Guice and T5-IoC and Spring to write the same application. I've just used Spring yet, but I'll play with Guice and T5-IoC as soon as I can. :)

xvik said...

This would be little offtopic, but im always want to know.. why you choose dojo for ajax and not google web toolkit?

Bob said...

This looks like Spring's JavaConfig, not Guice, and I think I prefer Spring's JavaConfig over Tapestry IoC. I don't think you fully appreciate how much extra code you're asking users to write and how obscure that code is. Then again, maybe we just use DI more pervasively.

If you want to know why Guice is so popular, the best advice I can give is, try it. Who knows? You might decide to lobby for a couple new features and adopt it, in which case you could spend more time on your web framework.

I'm not building a consulting practice around Guice. I don't want Guice to be a "Spring-killer," and it doesn't matter to me if you consider it a "real player in the IoC space." All that matters is that Guice enables me to write code how I want to write code. If that's not how you want to write code, there's always Spring/Tapestry, but I'm confident users will appreciate the higher level of maintainability Guice provides in the long run.

Unknown said...

It's a bit different from Spring Config, in that it understands that there are many modules out there.

A lot of framework design is knowing what areas users will accept "magic" and what areas they won't.

The arbitrary example I provided is not based on how T5 is normally used, but was twisted to around the Guice example. However, it could easily be streamlined into a reusable standalone module that would hide the details.

So ... in Guice, say I have a dependency that is an event source. When I create my service implementation (which implements the listener interface), how do I register my new service with the event source service?

I suspect in Guice you would have to inject the event source as a dependency, then have code in the new service to perform the registration.

There's a bunch of other areas where this model works very well, such as the cases where there is no service implementation class at build time, but it is generated at runtime. I do this a lot for common Gang-of-Four design patterns, such as chain of command.

So, what would a distributed contribution look like in Guice?

Bob said...

Are you asking me to address your use case, or do you want to know about distributed configurations in general? If you want me to address the use case, can you please give a little more detail?

Bob said...

"There simply isn't a better language for descibing instantiating Java objects and invoking Java methods than Java itself."

With Guice, most of the time, you don't manually write injection code at all. In cases where you do need to instantiate objects or invoke methods manually, you implement Provider<T> and write code in Java.

There may not be a better language, but it is even better to not write the code at all if you don't have to.

Unknown said...

See, I used to have that position, that you should let the framework figure out how to autowire and configure your objects from plain cloth. But I grew out of it, since it always sets limits on what you can do (for instance, my event registration example, as well as the created-on-the-fly example). And the separation of concerns (service creation vs. service operation) is very valuable.

Still, it would be an interesting challenge to support an alternative approach to "binding" interfaces to implementation classes, and allowing the injection annotations to occur on the constructor.

A big downside to your approach is when you are trying to apply IoC techniques to classes that you don't have control over, and so can't add the necessary annotations.

Bob said...

That's why Guice supports both approaches: automatic wiring and explicit wiring. We understand that you sometimes need to manually wire an object, but that doesn't mean you should always write explicit wiring code. "A foolish consistency is the hobgoblin of little minds."

Bob said...

Sony, how would you inject dependencies into RedXyzService and BlueXyzService?

Jesse Kuhnert said...

sony mathew: I think by that point I'd rather have it done by "magic" again.

Now I can't tell if my classes are implementing a system concerned design pattern or a context/ioc specific interface/design pattern... =p

Jesse Kuhnert said...

Sony:

Sounds like you might have been doing something funky in your Tapestry app. I profiled a few small apps via yourkit recently and found that after gc running my memory footprint stayed at around 22 megs.

Of course this was using compiled OGNL statements so the temporary object graph you'd normally get doesn't exist anymore.

Good luck with your context pattern, I've personally found that the best kinds of context implementations are the kind where you don't know they exist.

Unknown said...

Sonny,

I don't have a perfect memory from TheServerSide, but my recollection was that I was willing to pay for some pain in terms of refactoring and the use of XML to gain the benefits of a true container, in terms of divorcing myself from concerns about service instantiation, configuration and cleanup. You can think of approaches such as Tapestry 5 IoC and Guice as "have my cake and eat it to." What's interesting is that over the last couple of years I've learned a bit more about the Java Memory Model and thread concurrency issue, which makes me even happier to shunt those issues out of my main code and let the container take care of proper thread safe, just in-time instantiation.

Your statically defined service network is fine, I'm sure, for you contained application ... but as a Tapestry user, you must be aware of the myriad extension points offered by Tapestry. HiveMind was created as a plugin or framework IoC container (for Tapestry) and that nuanced difference is something I've struggled to explain to quite a number of people. When the service network is described only in code, it is locked together in a way that doesn't allow any selective overriding.

Sony Mathew said...

Update: Memory Leak with our App was due to Tapestry caching being disabled - so as to profile our code's memory usage during load without our various frameworks' caches consuming any memory. This had the opposite effect - Hivemind/JavaAssist/OGNL go nuts caching new class defs, expressions etc. each time a page template is re-loaded. When caching is enabled - the memory leaks disappear. Not exactly the behavior one expects - but hopefully corrects any memory leak accusations.