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.
5 comments:
"Tapestry has requirements as a framework that other containers don't offer solutions to"
Could you provide an example? I work on Guice and I'd love it if you could tell us what we're missing! Certainly you're a fan of our API...
It'll be a while before I get to all the differences. You may need to take a peek at the tapestry-ioc documentation.
I absolutely have borrowed ideas from Guice. I think Guice brought a fresh, interface/annotation driven approach.
Main thing that's necessary are the service configurations, which are key to creating extensible, modular architectures.
Also, having the service scope be encapsulated inside the proxy is important. It means that I can inject a request-scope proxy into a singleton-scope service and have it work. Inside the proxy, message invocations are delivered to a per-thread service implementation.
I'm not sure I like how the service builder methods are done. Certainly, I can see the usefulness of hooks around service instantiation. However, in the example, it would seem to work just as well for Tapestry 5 IoC to create the TranslatorSourceImpl (as described by a ServiceBinder.bind call) and pass it to buildTranslatorSource (instead of a ServiceResources). Allowing buildTranslatorSource to do the instantiation/lookup itself provides flexibility in that it can be used as a pre- or post-instantiation hook. However, if post-instantiation is the more common case (and I suspect it is), this adds a fairly unrelated parameter (the ServiceResources) and some boilerplate code to the service builder method. There's probably more to it (different use cases, other things that can be done with ServiceResources, etc.), and I'd be interested to see a more complex example.
One other observation: why the use of specially named methods? Having bind and build* methods creates a non-obvious dependency, in that there's nothing (except maybe some JavaDoc) that declares the importance of those names. This would seem to be a good place to use annotations, which I think worked well for JUnit in the same way.
Even annotations represent some additional code that has to be written. Module classes exist to do one thing: build and configure services, so enforcing a naming convention is just a reasonable to me as requiring an annotation.
The point of the service builder methods is flexibility; later articles will cover service-building services, where you can create a useful service even if there's no implementing class.
What I had seen with HiveMind is that, int the XML, we had gone further and further into cryptic, declaritive approaches to defining services (especially those that are built using other services). In any case, I wanted something pure and clean: a callback method to build the service, whatever that means.
Of course, service builder methods are the exception, and the ServiceBinder and bind() method (ideas adapted from Guice) are now the norm.
Hi Howard,
I have a question - which I hope you can point me in the right direction. I have a bunch of service classes and service implementation classes which I can dynamically pair up. I then tried to do this in a loop
binder.bind(serviceInterface,
implementationClass);
However it fails to compile since Eclipse thinks I am trying to do
binder.bind(serviceInterface,builder);
since my services and implementations are using generics, I could almost see why Eclipse chooses the wrong bind method.
Is there an easy way to give a "hint" for Eclipse to choose the right "bind"?
I thought I could not be the only one asking this question, if you know where the answer lies, would appreciate it very much if you can just point me to it.
Thanks,
James
PS this block code illustrates what I am trying to do in the AppModule
System.out.println("Scanning Package "+DataAccessServicesPackage+"...");
Reflections reflections = new Reflections(DataAccessServicesPackage);
Set> allServiceImplClasses = reflections.getSubTypesOf(SimpleDataAccessObjectImpl.class);
Set> allServiceClasses = reflections.getSubTypesOf(SimpleDataAccessObject.class);
for (Class serviceImplClass:allServiceImplClasses) {
for (Class serviceClass:allServiceClasses) { if (
Arrays.asList(serviceImplClass.getInterfaces()).contains(serviceClass)) {
System.out.print("--------Class ");
System.out.print(StringUtils.uncapitalize(serviceImplClass.getSimpleName()));
System.out.print(" implements ");
System.out.println(StringUtils.uncapitalize(serviceClass.getSimpleName()));
binder.bind(serviceClass,serviceImplClass);
}
}
}
Post a Comment