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:
- businessPortal = new PortalBuilder()
- businessPortal.entry {
- corporateMessages (TOP)
- navigationBar (LEFT)
- content (CENTER) {
- keyAccounts()
- }
- advertisements (RIGHT)
- stockTicker (BOTTOM)
- meetingReminder (POPUP)
- }
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:
- query = new QueryBuilder()
- query.table(customer:'c', invoice:'i') {
- join (c:'invoice_id', i:'id')
- and {
- greater('i.total': 1000)
- like ('c.name' : '%Bill%')
- }
- }
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:
That means a code fragment like:
- builder = new MyBuilder()
- builder.foo() {
- bar(a:1)
- }
- builder = new MyBuilder()
- foo = builder.createNode('foo')
- // no setParent() call since we are a root node
- bar = builder.createNode('bar',[a:1])
- builder.setParent(foo, bar)
- // no closure to process for bar
- builder.nodeCompleted(foo, bar)
- builder.nodeCompleted(null, foo)
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
- class DebugBuilder extends BuilderSupport {
- def result = ''<<'' // Empty StringBuffer
- def indent = ' ' * 4
- int indentCount = -1
- def createNode(name){ // Builder calls goes through this method
- indentCount++
- return check(name)
- }
- def createNode(name, value){
- return check(createNode(name) << format(value))
- }
- def createNode(name, Map attributes){
- return check(createNode(name) << format(attributes))
- }
- def createNode(name, Map attributes, value){
- return check(createNode(name, attributes) << format(value))
- }
- void setParent(parent, child){
- result << "\n" << indent*indentCount << child.toString()
- }
- void nodeCompleted(parent, node) {
- indentCount--
- }
- private check(descr){
- if (!current) result << descr // Special root handling
- return descr
- }
- private format(value) {
- return '(' << value.toString() << ')'
- }
- private format(Map attributes) {
- StringBuffer formatted = '' << '['
- attributes.each { key, value ->
- formatted << key << ':' << value << ', '
- }
- formatted.length = formatted.size() - 2
- formatted << ']'
- return formatted
- }
- }
- def builder = new DebugBuilder()
- builder.foo(){
- bar()
- baz('x') { map(a:1) }
- }
- assert "\n" + builder.result == '''
- foo
- bar
- baz(x)
- map[a:1]'''
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:
- builder = new DebugBuilder(result: new PrintWriter(System.out))
- …
- builder.result.flush()
* Documentation - Advance User Guide - Make a builder
沒有留言:
張貼留言