程式扎記: [ In Action ] Working with builders - Creating your own builder

標籤

2014年7月7日 星期一

[ In Action ] Working with builders - Creating your own builder

Preface: 
The built-in builders are useful, but they aren’t tailored for your specific needs. Given how frequently hierarchies are used within software development, it wouldn’t be surprising to find that you had a domain-specific use for builders that isn’t quite covered with NodeBuilder and its colleagues. Fortunately, Groovy makes it easy to build your own builder (which isn’t as recursive as it sounds). We’ll give you a few examples of why you might want to write your own builder and go through the support Groovy gives you. 

Suppose you are creating an application that serves different user groups and supplies each with a customized portal. You may want do specify the portal for your business user group. Imagine a builder like: 
  1. businessPortal = new PortalBuilder()  
  2. businessPortal.entry {  
  3.     corporateMessages (TOP)  
  4.     navigationBar     (LEFT)  
  5.     content           (CENTER) {  
  6.         keyAccounts()  
  7.     }  
  8.     advertisements    (RIGHT)  
  9.     stockTicker       (BOTTOM)  
  10.     meetingReminder   (POPUP)  
  11. }  
Such a builder would give you the opportunity to use your specification regardless of the underlying technology. It could be used for plain HTML, portlets, Rich Internet Applications, and even for a Swing client. It’s only a matter of how to implement the builder. 

Note that such a specification is more flexible than one that resides in a fixed data-structure or in an XML file: You can use variables, constants, method calls, and any other Groovy feature. A second idea: Suppose you have a technical application such as a persistence framework with the feature to shield your users from SQL. Imagine building a hypothetical query for customers named like Bill with invoices greater than $1000, like this: 
  1. query = new QueryBuilder()  
  2. query.table(customer:'c',   invoice:'i') {  
  3.     join   (c:'invoice_id', i:'id')  
  4.     and {  
  5.         greater('i.total'1000)  
  6.         like   ('c.name' : '%Bill%')  
  7.     }  
  8. }  
The builder could map this specification to the SQL dialect you are using or to special types of your persistence framework (such as selection criteria). 

Implementing these examples is beyond the scope of this book, but we hope to give you all the builder knowledge you require in order to create them for yourself. This section shows how well Groovy supports implementing builders, taking you through an instructive example. 

Subclassing BuilderSupport: 
All builders in the Groovy library are subclasses of groovy.util.BuilderSupport. This class implements the general builder strategy: allowing you to pretend your builder methods, to recursively process any attached closures, to relay method calls in closures back to your builder, and to call your builder’s template methods. 

To implement your own builder, you subclass BuilderSupport and implement the template methods as listed in table 8.8. 
 

BuilderSupport follows this construction algorithm: 
1. When hitting a builder method, call the appropriate createNode method.
2. Call setParent with the current parent and the node you’ve just created (unless it’s a root node, which has no parent).
3. Process any attached closure (this is where recursion happens).
4. Call nodeCompleted with the current parent and the created node (even if parent is null ).

That means a code fragment like: 
  1. builder = new MyBuilder()  
  2. builder.foo() {  
  3.     bar(a:1)  
  4. }  
will result in method calls like (pseudocode; indentation indicates recursion depth
  1. builder = new MyBuilder()  
  2. foo = builder.createNode('foo')  
  3. // no setParent() call since we are a root node  
  4.     bar = builder.createNode('bar',[a:1])  
  5.     builder.setParent(foo, bar)  
  6.         // no closure to process for bar  
  7.     builder.nodeCompleted(foo, bar)  
  8. builder.nodeCompleted(null, foo)  
Note that the foo and bar variables are not used inside the real builder. They are used in this pseudocode only for illustrating identities. 

In terms of the implementation, nodeCompleted isn’t a template method in the strict meaning of the word, because it is not declared abstract in BuilderSupport but has an empty default implementation. However, it is added to table 8.8 because most builders need to override it anyway. 

Further methods of BuilderSupport are listed in table 8.9. See their API documentation for more details. 
 

The DebugBuilder example: 
Our example of how to implement a self-made builder is aimed at being as close to the point as possible. It does little more than reveal how your builder methods were called and is therefore named DebugBuilder

Despite looking like a textbook example, it is of practical relevance. Let’s assume you write an automation script with AntBuilder that behaves unexpectedly. You can then use DebugBuilder in place of AntBuilder to find out whether your Ant tasks were called in the expected sequence with the expected values. 

Listing 8.14 contains the implementation of DebugBuilder as a subclass of BuilderSupport with a trailing script that shows how to use it and asserts its behavior. In the process of building, all relevant information about node creation is 
appended to a result property whenever setParent is called. Because this never happens for the root node, a check method recognizes the creation of the root node. 
- Listing 8.14 Using BuilderSupport for DebugBuilder 
  1. class DebugBuilder extends BuilderSupport {  
  2.     def result = ''<<''       // Empty StringBuffer  
  3.     def indent = ' ' * 4  
  4.     int indentCount = -1  
  5.     def createNode(name){   // Builder calls goes through this method  
  6.         indentCount++  
  7.         return check(name)  
  8.     }  
  9.     def createNode(name, value){  
  10.         return check(createNode(name) << format(value))  
  11.     }  
  12.     def createNode(name, Map attributes){  
  13.         return check(createNode(name) << format(attributes))  
  14.     }  
  15.     def createNode(name, Map attributes, value){  
  16.         return check(createNode(name, attributes) << format(value))  
  17.     }  
  18.     void setParent(parent, child){  
  19.         result << "\n" << indent*indentCount << child.toString()  
  20.     }  
  21.     void nodeCompleted(parent, node) {  
  22.         indentCount--  
  23.     }  
  24.       
  25.     private check(descr){  
  26.        if (!current) result << descr  // Special root handling  
  27.        return descr  
  28.     }  
  29.     private format(value) {  
  30.         return '(' << value.toString() << ')'  
  31.     }  
  32.     private format(Map attributes) {  
  33.         StringBuffer formatted = '' << '['  
  34.         attributes.each { key, value ->  
  35.             formatted << key << ':' << value << ', '  
  36.         }  
  37.         formatted.length = formatted.size() - 2  
  38.         formatted << ']'  
  39.         return formatted  
  40.     }  
  41. }  
  42.   
  43. def builder = new DebugBuilder()  
  44. builder.foo(){  
  45.     bar()  
  46.     baz('x') { map(a:1) }  
  47. }  
  48. assert "\n" + builder.result == '''  
  49. foo  
  50.     bar  
  51.     baz(x)  
  52.         map[a:1]'''  
The nesting depth of recursive closure calls is reflected by indenting the according lines when appending to the result. This depth is increased on any call to createNodeand decreased on nodeCompleted

The final assertion in listing 8.14 suggests that DebugBuilder can generally be used to support unit-testing with builders. For example, by injecting a DebugBuilder into code that expects a SwingBuilder, you can use such an assertion to unit-test that code. 

DebugBuilder also supports duck typing with the result property. Because the only operator that is ever applied to result is the << leftshift operator, the result property can be set to an object of any type that supports that operator. The default is a StringBuffer , but it can be set to an array, any collection, or even a Writer . For example, to print the results to the console, use the following: 
  1. builder = new DebugBuilder(result: new PrintWriter(System.out))  
  2. …  
  3. builder.result.flush()  
Supplement: 
Documentation - Advance User Guide - Make a builder

沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!