Tapestry 5 includes its own internal Inversion of Control container. This is often a point of contention ... why not just use Spring or (in more recent conversations) Guice?
That's a complex question; simply put, Tapestry has requirements as a framework that the other containers don't offer solutions to.
This posting is a simple introduction to the basics of Tapestry 5 IoC. In later postings, we'll get into more detail about the advanced features of Tapestry's IoC container, the ones that really distance it from Spring and Guice.
Tapestry uses the term "service" for the primary objects that it manages for you. Spring uses the term "bean". A service is normally an interface and a class that implements the interface. In the most typical case, only a single service implements the interface, but T5 IoC is fully capable of handling the case where one service interface has a number of distinct services; even the case where a single class is instantiated with a different configuration.
Every service has a unique id string. In most cases, this is just the simple name of the service interface. When the same interface is used by multiple services, you will have to identify the service id explicitly.
To keep things real, I'll use actual, though abbreviated, examples from Tapestry's code base.
T5 IoC uses module classes to identify what services are available. A module class is a POJO class with a special method on it, a method named bind()
. A Tapestry application will consist of a number of modules: some modules provided by Tapestry itself, some by third party libraries or extensions, and some by the application itself. Tapestry mixes and matches all of this information, all of the services defined by each of the modules, into a single registry of services.
That may sound more complex than it really is. The reality is that in the bind()
method, we simply match service interfaces to corresponding implementations:
public final class TapestryModule
{
public static void bind(ServiceBinder binder)
{
binder.bind(ClasspathAssetAliasManager.class, ClasspathAssetAliasManagerImpl.class);
binder.bind(PersistentLocale.class, PersistentLocaleImpl.class);
binder.bind(ApplicationStateManager.class, ApplicationStateManagerImpl.class);
// ... and so on
}
}
The ServiceBinder is uses generics to ensure that the class you specify implements the service interface. The API is a fluent interface: you can chain a few extra method calls onto bind() to override defaults, for example:
binder.bind(ObjectProvider.class, AssetObjectProvider.class).withId("AssetObjectProvider");
TapestryModule actually defines quite a few additional services.
Let's look at an example:
public interface PersistentLocale
{
void set(Locale locale);
Locale get();
boolean isSet();
}
I've stripped out the comments to save space ... but this service manages the user's locale; it's a key part of Tapestry 5's localization support. The implementation we'll see shortly works using HTTP Cookies, but that isn't important to the code that uses PersistentLocale.
public class PersistentLocaleImpl implements PersistentLocale
{
private static final String LOCALE_COOKIE_NAME = "org.apache.tapestry5.locale";
private final Cookies cookieSource;
public PersistentLocaleImpl(Cookies cookieSource)
{
this.cookieSource = cookieSource;
}
public void set(Locale locale)
{
cookieSource.writeCookieValue(LOCALE_COOKIE_NAME, locale.toString());
}
public Locale get()
{
String localeCookieValue = getCookieValue();
return localeCookieValue != null ? LocaleUtils.toLocale(localeCookieValue) : null;
}
private String getCookieValue()
{
return cookieSource.readCookieValue(LOCALE_COOKIE_NAME);
}
public boolean isSet()
{
return getCookieValue() != null;
}
}
T5 IoC does all injection through the constructor. This is to encourage you to write your dependencies into final fields, which is thread safe. Typically, your services will be immutable objects: all fields final.
PersistentLocaleImpl has a dependency on another service, Cookies. And what is Cookies? It's another service interface. Notice that we don't have to do any extra configuration here ... since there's one, and only one, service that implements the Cookies interface, that's all the information Tapestry needs to wire things together.
Other service implementations inside Tapestry have as few as zero dependencies, and as many as eight. There's no theoretical limit, it's just that having more than a few dependencies is a design smell ... that you can break things into smaller pieces.
One of the hallmarks of coding using an IoC container is this level of terseness, also knows as passing the buck. Given that PersistentLocaleImpl is concerned with HTTP cookies, you'd think that it would, somehow, get ahold of the HttpServletRequest object and start invoking getCookies()
and addCookie()
on it ... but instead, all the details of interfacing with the Servlet API and the rather awkward API for HTTP cookies is swept into a corner, inside the Cookies service implementation.
That's great ... it makes the implementation of PersistentLocaleImpl (as well as any other code that happens to care about HTTP cookies) that much simpler and easier to test.
Service Lifecycle
Tapestry services have a specific lifecycle:
- defined
- Identified via the ServiceBinder, but not yet referenced
- virtual
- A proxy exists that has been injected as a dependency of some other service, but no methods of the proxy have been invoked
- realized
- The service has been instantiated with dependencies
The beauty of this is that your code is completely unaware of this; all the work inside Tapestry ... creating proxies, realizing service implementations, occurs in a lazy but thread-safe manner. It's as if all the services are instantiated at startup without taking the time to actually do that work.
Again, the appeal of an IoC container is that you get to break your application into tiny, easily tested bits, and the IoC container is responsible for connecting everything back together at runtime. It really leads to a new way of coding, and thinking about coding.
Service Builder Methods
Sometimes just instantiating a class is not enough; there may be additional configuration needed as part of instantiating the class. Tapestry 5 IoC's predecessor, HiveMind, accomplished such goals with complex service-building services. It ended up being a lot of XML.
T5 IoC accomplishes the same, and more, using service builder methods; module methods that construct a service. A typical case is when a service implementation needs to listen to events from some other service:
public static TranslatorSource buildTranslatorSource(ComponentInstantiatorSource componentInstantiatorSource,
ServiceResources resources)
{
TranslatorSourceImpl service = resources.autobuild(TranslatorSourceImpl.class);
componentInstantiatorSource.addInvalidationListener(service);
return service;
}
Module methods prefixed with "build" are service builder methods. The service interface is defined from the return value (TranslatorSource). The service id is explicitly "TranslatorSource" (that is, everything after "build" in the method name).
Here, Tapestry has injected into the service builder method. ComponentInstantiatorSource is a service that fires events. ServiceResources is something else: it is a bundle of resources related to the service being constructed ... including the ability to instantiate an object including dependencies. What's great here is that buildTranslatorSource()
doesn't need to know what the dependencies of TranslatorSourceImpl are, it can instantiate the
class with dependencies using the autobuild()
method. The service builder then adds the new service as a listener of the ComponentInstantiatorSource, before returning it.
This is a great separation of concerns: we have a construction concern (being an event listener) that's distinct from the operational concerns of TranslatorSource. And they are kept separate.
Conclusion
Tapestry IoC has simple and concise API for defining services and, in most cases, handles dependencies automatically. The end result is that it becomes child's play to divide-and-conquer: convert old, monolithic, hard to maintain code into small, easily tested, easily understood services.
In future postings, I'll go into more detail about the more advanced features of Tapestry: service scopes, service configurations and service decorations.