Paul Hammant's Blog: A Spring Framework Shortcoming
Spring is awesome of course, but there’s a shortcoming that I think the Spring team could eliminate with some modest work. I know it can be done because colleagues Jon Wolter and Lucas Ward did it at a ThoughtWorks client site after I had discussed it with them. The shortcoming I am talking about is the lack of support for collaborator components for request mapped injections. I’m not going to explain how Spring can be enhanced to allow it, just why it is desirable. The symptom of the problem is the presence of an inheritance hierarchy for controllers. In the Agile age of course we prefer Composition over Inheritance.
Consider a component-centric shopping web-app. Its directed graph of dependency injections would be illustrated like so:
(Application Scope has another name in Spring, but I can’t bring myself to say it).
Lets think about the a checkout user action causing the app to offer one last up-sell opportunity:
“Red Lipstick #59 really goes with Dremel ‘MiniMite’; So says 93 customers. Just $4.99<buy it, you know you want to>”.
The CheckoutController may have a bunch of dependencies at the instance level (Cart, Order, Promotions). Cart was created and filled by some other controllers interactions from previous HTTP requests. Given this is a checkout action, Order is now being created from Cart contents, and having other related data filled in over a series of interactions.
If we drill into the relationship between CheckoutController and Promotions, we see that for the products in the cart, the Promotions instance (think PromotionsManager if you’re that way inclined) is like so:
pseudo-code: give me a promotion or two for a person buying these items
The interaction is between a request scoped component and an application scoped one, with no session scoped components in play for it. It need not be though (the point of this blog entry). What if there were an intermediate collaborator, called UpSell (or UpSellOffer) that was injected into the applicable request scoped method of CheckoutController? If there were then it would mean that CheckoutController would not need to know about Promotions any more. Directed graph of injections like so:
We see that UpSell is a request scoped component. That means its lifecycle ends with the request. If the user chooses to buy it too after being enticed by the message, then it goes into the cart as a regular cart item. Thus, it is not a session scoped thing, it is another transient request scoped thing. Being request scoped, it can take advantage of direct injections of what it needs.
What we found was that Spring did not want to take arbitrary request scoped components in the method parameters of a request-mapped method. In my opinion it should do, so Jon and Lucas went about making it work and caching instances at that scope for the duration of the request. The created another annotation @AutowiredHandler and used that at the method parameter level. Spring still did its thing at the constructor and setter injection level, but there was suddenly one more place where injections of suitably scoped components could happen.
You will note of course that UpSell is a subjective thing. For UpSell the team created a UpSellFactoryBean (that took dependencies) that would create the UpSell instance that would be cached for scope and injected where needed. This way different end-users would receive different UpSell instances as required.
The need for this component fidelity is rooted in two maxims, and a side benefit:
1) Separation of Concerns (SoC)
Inversion of Control recommended thinking about SoC when engineering larger component-based systems. SoC makes for better de-composition or componentization of an application.
2) Better unit tests
Unit tests should be small focussed things. One or two lines of setup per thing being tested in each test method, one line of interaction and ideally one assert is ideal. Developers can go insane setting up mocks for some controllers with multiple service injections. Never mind understanding them after they were written! (sarcasm)
3) Less inheritance for Spring Controllers.
When service lookups begin to affect multiple Spring controllers, you’ll see such access logic migrate towards parent classes. We’re all preferring composition over inheritance these days right?
Creating the collaborator UpSell and its factory bean mean we get closer to (1) and (2), which makes Agile developers at least happier.
There are other considerations. One is what to do if no UpSells are appropriate or the Promotions Service is failing presently, but the checkout should continue regardless. In this case there are choices. One would be static instances like UpSell.NOT_AVAILABLE and UpSell.NOT_APPROPRIATE. This is a workable solution but it would would lead to some unsavory if/else logic somewhere downstream. Better perhaps is some functor/visitor logic in UpSell that does not require conditionals in page logic.
Lastly, many folks would engineer their dependency-injection apps to have the maximum amount of components registered at application scope, and this request scope design would be alien to them. There is also the aop:scoped-proxy scope which is more transient than request, but feels hacky to me.
Anyway, it is time Jon and/or Lucas wrote about this themselves :)
Nov 9, 2010
Nov 22, 2010 update: Here’s Jon’s posting on his @AutowiredHandler work with Lucas
Aug 31, 2011 update: Read also Ted Young’s AOPScope + FactoryBean tricks towards the same end