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 Invoice, LineItem, 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
- def builder = new NodeBuilder() // 1) Builder creation
- def ulcDate = new Date(107,0,1)
- def invoices = builder.invoices{ // 2) Root node creation
- invoice(date: ulcDate){ // 3) Invoice creation
- item(count:5){
- product(name:'ULC', dollar:1499)
- }
- item(count:1){
- product(name:'Visual Editor', dollar:499)
- }
- }
- invoice(date: new Date(106,1,2)){
- item(count:4) {
- product(name:'Visual Editor', dollar:499)
- }
- }
- }
- // 4) GPath query
- def soldAt = invoices.grep {
- it.item.product.any{ it.'@name' == 'ULC' }
- }.'@date'
- assert soldAt == [ulcDate]
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:
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-y—you 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
- System.setProperty("user.timezone","CET")
- def builder = new NodeBuilder()
- def invoices = builder.invoices {
- for(day in 1..3) {
- invoice(date: new Date(107,0,day)){
- item(count:day){
- product(name:'ULC', dollar:1499)
- }
- }
- }
- }
- def writer = new StringWriter()
- invoices.print(new PrintWriter(writer))
- def result = writer.toString().replaceAll("\r","")
- assert "\n"+result == """
- invoices() {
- invoice(date:Mon Jan 01 00:00:00 CET 2007) {
- item(count:1) {
- product(name:'ULC', dollar:1499)
- }
- }
- invoice(date:Tue Jan 02 00:00:00 CET 2007) {
- item(count:2) {
- product(name:'ULC', dollar:1499)
- }
- }
- invoice(date:Wed Jan 03 00:00:00 CET 2007) {
- item(count:3) {
- product(name:'ULC', dollar:1499)
- }
- }
- }
- """
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:
- println invoices.depthFirst()*.name()
沒有留言:
張貼留言