程式扎記: [ In Action ] Working with builders - Building object trees with NodeBuilder

標籤

2014年6月16日 星期一

[ In Action ] Working with builders - Building object trees with NodeBuilder

Preface: 
We start the more detailed explanation of builders with the same example we used in section 7.5.1 to demonstrate GPath: modeling Invoices with LineItems andProducts. We will build a runtime structure of nodes rather than specialized business objects and watch the building process closely. You will learn not only about how NodeBuilder works, but also how the general principle of builders is applied in Groovy. We will then consider how the declarative style of builder use can be freely mixed with normal logic. Builders can be used without any special knowledge, but in order to understand how they work, it is a prerequisite to know about pretended and relayedmethods (section 7.6) and closure scoping (section 5.5). 

Based on our invoice example from section 7.5.1, we set out to build a runtime object structure as depicted in figure 8.1. 
 

In listing 7.23, we built this runtime structure with three defined classes InvoiceLineItem, and Product and through calling their default constructors in a nested manner.

NodeBuilder in action—a closer look at builder code 
Listing 8.4 shows how to implement the invoice example using a NodeBuilder. The NodeBuilder can replace all three of our classes, assuming that we’re just treating them as data storage types (that is, we don’t need to add methods for business logic or other behavior). Also added is a final GPath expression to prove that we can still walk conveniently through the object graph. This is the same query we used in section 7.5.1. Note how the tree structure from figure 8.1 is reflected in the code! 
- Listing 8.4 Invoice example with NodeBuilder 
  1. def builder = new NodeBuilder()                         // 1) Builder creation  
  2. def ulcDate = new Date(107,0,1)  
  3. def invoices = builder.invoices{                        // 2) Root node creation  
  4.     invoice(date: ulcDate){                             // 3) Invoice creation  
  5.         item(count:5){  
  6.             product(name:'ULC', dollar:1499)  
  7.         }  
  8.         item(count:1){  
  9.             product(name:'Visual Editor', dollar:499)  
  10.         }  
  11.     }  
  12.     invoice(date: new Date(106,1,2)){  
  13.         item(count:4) {  
  14.             product(name:'Visual Editor', dollar:499)  
  15.         }  
  16.     }  
  17. }  
  18.   
  19. // 4) GPath query  
  20. def soldAt = invoices.grep {  
  21.     it.item.product.any{ it.'@name' == 'ULC' }  
  22. }.'@date'  
  23. assert soldAt == [ulcDate]  
We make a new instance of the NodeBuilder for later use at (1), and then we call the invoices method on the NodeBuilder instance at (2). This is a pretended method: The NodeBuilder intercepts the method call. It constructs a node based on the name of the intercepted method name and returns it into the invoices variable. Before the node is constructed, the trailing closure is called to construct its nested nodes. To make this possible, the BuilderSupport that NodeBuilder inherits from sets the closure’s delegate to the NodeBuilder instance. 

The invoice method call is relayed to the NodeBuilder instance in (3), because it is the current closure’s delegate. This method also takes a map as a parameter. The content of this map describes the attributes of the constructed node. 

Finally, we need to adapt the GPath a little to use it in (4). First, we’ve broken it into multiple lines to allow proper typesetting in the book. Second, node attributes are no longer accessible as properties but as map entries. Therefore, product.name now becomes product['@name'] or, even shorter, product.'@name'

The additional at-sign is used for denoting attributes in analogy to XPath attribute conventions. A third change is that through the general handling mechanism of nodes, item.product is now a list of products, not a single one. 

Understanding the builder concept: 
From the previous example, we extract the following general rules: 
* Nodes are constructed from pretended method calls on the builder.
* Method names determine node names.
* When a map is passed as a method argument, it determines the node’s attributes. Generally speaking, each key/value pair in the map is used to call the field’s setter method named by the key with the value. This refinement will later be used with SwingBuilder to register event listeners.
* Nesting of nodes is done with closures. Closures relay method calls to the builder.

This concept is an implementation of the Builder pattern (GOF). Instead of programming how some tree-like structure is built, only the result, the what, is specified. Thehow is left to the builder. Note that only simple attribute names can be declared in the attribute map without enclosing them in single or double quotes. Similarly, node names are constructed from method names, so if you need names that aren’t valid Groovy identifiers—such as x.y or x-yyou will again need to use quotes. So far, we have done pretty much the same as we did with hand-made classes, but without writing the extra code. This is already a useful advantage, but there is more to come. 

Smart building with logic: 
With builders, you can mix declarative style and Groovy logic, as listing 8.5 shows. We create nested invoices in a loop for three consecutive days, with sales of the product growing each day. To assess the result, we use a pretty-printing facility available for nodes. 
- Listing 8.5 Using logic inside the NodeBuilder 
  1. System.setProperty("user.timezone","CET")  
  2. def builder = new NodeBuilder()  
  3. def invoices = builder.invoices {  
  4.     for(day in 1..3) {  
  5.         invoice(date: new Date(107,0,day)){  
  6.             item(count:day){  
  7.                 product(name:'ULC', dollar:1499)  
  8.             }  
  9.         }  
  10.     }  
  11. }  
  12. def writer = new StringWriter()  
  13. invoices.print(new PrintWriter(writer))  
  14. def result = writer.toString().replaceAll("\r","")  
  15. assert "\n"+result == """  
  16. invoices() {  
  17.   invoice(date:Mon Jan 01 00:00:00 CET 2007) {  
  18.     item(count:1) {  
  19.       product(name:'ULC', dollar:1499)  
  20.     }  
  21.   }  
  22.   invoice(date:Tue Jan 02 00:00:00 CET 2007) {  
  23.     item(count:2) {  
  24.       product(name:'ULC', dollar:1499)  
  25.     }  
  26.   }  
  27.   invoice(date:Wed Jan 03 00:00:00 CET 2007) {  
  28.     item(count:3) {  
  29.       product(name:'ULC', dollar:1499)  
  30.     }  
  31.   }  
  32. }  
  33. """   
Nodes as constructed with the NodeBuilder have some interesting methods, as listed in table 8.1. Note that these methods being present on the nodes doesn’t prevent you from having nodes of the same name (such as a node called iterator)—you build child nodes by calling methods on the NodeBuilder , not on the nodes themselves. For a complete and up-to-date description, look at Node ’s API documentation at http://groovy.codehaus.org/apidocs/groovy/util/Node.html
 

Nodes are used throughout the Groovy library for transparently storing tree-like structures. You will see further usages with XmlParser in section 12.1.2

With this in mind, you may want to have some fun by typing: 
  1. println invoices.depthFirst()*.name()  
That’s all there is to NodeBuilder. It makes a representative example for all builders in the sense that whenever you use a builder, you create a builder instance and call methods on it with attached nested closures that result in an object tree. 

沒有留言:

張貼留言

網誌存檔

關於我自己

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