본문 바로가기

Spring Framework

A Spring Framework Shortcoming

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 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.

Consider a component-centric shopping web-app. Its directed graph of dependency injections would be illustrated like so:

directed graph of deps 1
(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 previously.  Order is being created presently from the Cart, 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:

directed graph with upsell in req scope

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.

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 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 the test method, one line of interaction and ideally one assert is ideal.  Developers can go nuts setting up mocks for some controllers with multiple service injections. Never mind understanding it after it was written!

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 prefering composition over inheritence 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 it 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 unsavorary if/else logic somewhere downstream.   Better perhaps is some functor/visitor logic in UpSell that does not require conditionals in page logic.

Anyway, it is time Jon and/or Lucas wrote about this themselves :)

Nov 9, 2010