Thursday, December 27, 2007

Fluent API pattern: Nested Interfaces

Multiple Fluent APIs can be used in a nested fashion in order to separate and organize responsibilities while keeping individual classes simple. Fluent APIs can become complex as more features are added if a designer is not careful. This pattern gives a clear and sustainable path for growth. Let's take a closer look at one of the examples from my last post:

Customer customer = createCustomer.named("Bob").that(bought.item(CompactDisc).on("02/17/2006"));

Reading the words reveals in English exactly what is going on with the code. It creates a customer named Bob that bought a compact disc on 02/17/2006. What is going on behind the scenes? That is not quite as clear, nor is it important to anyone other than the Fluent API designer!

Before diving in, let's talk briefly about the domain. We are creating a Customer. Customer's have Transactions. Transactions have Items and Dates.

The first thing to notice is that almost half of this code is fully enclosed in parenthesis. The result of 'bought.item(CompactDisc).on("02/17/2006")' is being passed to a method named 'that'. It is not obvious here, but the 'that' method is on a CustomerCreator object.

Now notice that 'bought' is an object. However, in this snippet this object is not instantiated. Don't be too bothered by this. The declaration and instantiation of this object is just fluff that distracts from the true intent of the code. With some creative use of static imports you can instantiate this object in just one place in your entire codebase and never have to look at it again. Know, though, that the 'bought' object is a TransactionCreator.

TransactionCreator has methods that set Items and Date. In order to chain methods, all methods on TransactionCreator return a TransactionCreator with all appropriate data set. Warning: Do not just return 'this' from such methods. You will need to do a deep clone or you will likely have very interesting things happen unexpectedly when you change data on one Transaction and it changes on all the others as well.

The object 'createCustomer' is very similar of type CustomerCreator. It creates a Customer instead of a Transaction. However, it follows the exact same pattern as TransactionCreator. Methods 'named' and 'that' are both on the CustomerCreator class and both return cloned CustomerCreators that have had some piece of data added to it.

The final piece of the puzzle is that CustomerCreator and TransactionCreator extend Customer and Transaction respectively. This allows for method chaining that can end at any time and still give you an object of the target class.

I strongly encourage you to download the full example source and look through it. Fluent APIs do not need to be overly complex, but they are a drastic shift from traditional interface design. Don't give up even if it takes a while to really understand. The benefits are worth the learning curve.

No comments: