程式扎記: [ In Action ] Dynamic object orientation - Using power features

標籤

2014年4月10日 星期四

[ In Action ] Dynamic object orientation - Using power features

Preface: 
This section presents three power features that Groovy supports at the language level: GPath, the Spread operator, and the use keyword. 

We start by looking at GPaths. A GPath is a construction in Groovy code that powers object navigation. The name is chosen as an analogy to XPath, which is a standard for describing traversal of XML (and equivalent) documents. Just like XPath, a GPath is aimed at expressiveness: realizing short, compact expressions that are still easy to read. GPaths are almost entirely built on concepts that you have already seen: field access, shortened method calls, and the GDK methods added to Collection. They introduce only one new operator: the *. spread-dot operator. Let’s start working with it right away. 

Querying objects with GPaths: 
We’ll explore Groovy by paving a path through the Reflection API. The goal is to get a sorted list of all getter methods for the current object. We will do so step-by-step, so please open a groovyConsole and follow along. You will try to get information about your current object, so type: 
and run the script (by pressing Ctrl-Enter). In the output pane, you will see something like: 
groovy> this
Result: ConsoleScript1@2a134eca

which is the string representation of the current object. To get information about the class of this object, you could use this.getClass , but in Groovy you can type: 
which displays (after you run the script again): 
groovy> this.class
Result: class ConsoleScript2

The class object reveals available methods with getMethods , so type: 
  1. this.class.methods  
which prints a long list of method object descriptions. This is too much information for the moment. You are only interested in the method names. Each method object has agetName method, so call: 
  1. this.class.methods.name  
and get a list of method names, returned as a list of string objects. You can easily work on it applying what you learned about strings, regular expressions, and lists. Because you are only interested in getter methods and want to have them sorted, type: 
  1. this.class.methods.name.grep(~/get.*/).sort()  
you will get the result: 
groovy> this.class.methods.name.grep(~/get.*/).sort()
Result: [getBinding, getClass, getMetaClass, getProperty]

Such an expression is called a GPathOne special thing about it is that you can call the name property on a list of method objects and receive a list of string objects—that is, the names. The rule behind this is that: 
is equal to: 
  1. list.collect{ item -> item?.property }  
This is an abbreviation of the special case when properties are accessed on lists. The general case reads like 
where *. is called the spread-dot operator and member can be a field access, a property access, or a method call. The spread-dot operator is needed whenever a method should be applied to all elements of the list rather than to the list itself. It is equivalent to: 
  1. list.collect{ item -> item?.member }  
To see GPath in action, we step into an example that is reasonably close to reality. Suppose you are processing invoices that consist of line items, where each line refers to the sold product and a multiplicity. A product has a price in dollars and a name. An invoice could look like table 7.3. 
 

Figure 7.1 depicts the corresponding software model in a UML class diagram. The Invoice class aggregates multiple LineItems that in turn refer to a Product
 

Listing 7.23 is the Groovy implementation of this design. It defines the classes as GroovyBeans, constructs sample invoices with this structure, and finally uses GPath expressions to query the object graph in multiple ways. 
- Listing 7.23 Invoice example for GPath 
  1. // Set up data structures  
  2. class Invoice {  
  3.     List    items  
  4.     Date    date  
  5. }  
  6. class LineItem {  
  7.     Product product  
  8.     int     count  
  9.     int total() {return product.dollar * count}  
  10. }  
  11. class Product {  
  12.     String  name  
  13.     def     dollar  
  14. }  
  15.   
  16. // Fill with sample data  
  17. def ulcDate = new Date(107,0,1)  
  18. def ulc = new Product(dollar:1499, name:'ULC')  
  19. def ve  = new Product(dollar:499,  name:'Visual Editor')  
  20. def invoices = [  
  21.     new Invoice(date:ulcDate, items: [  
  22.         new LineItem(count:5, product:ulc),  
  23.         new LineItem(count:1, product:ve)  
  24.     ]),  
  25.     new Invoice(date:[107,1,2], items: [  
  26.         new LineItem(count:4, product:ve)  
  27.     ])  
  28. ]  
  29.   
  30. // 1) Total for each line item  
  31. assert [5*14994994*499] == invoices*.items*.collect{it.total()}.flatten()   
  32.   
  33. // 2) Query of product names  
  34. assert ['ULC'] == invoices*.items*.grep{it.total() > 7000}.product.name.flatten()  
  35.   
  36. // 3) Query of invoice date  
  37. def searchDates = invoices.grep{  
  38.     it.items.any{it.product == ulc}  
  39. }.date*.toString()  
  40. assert [ulcDate.toString()] == searchDates  
The queries in listing 7.23 are fairly involved. The first, at (1), finds the total for each invoice, adding up all the line items. We then run a query, at (2), which finds all the names of products that have a line item with a total of over 7,000 dollars. Finally, query (3) finds the date of each invoice containing a purchase of the ULC product and turns it into a string. 
Note. 
invoices*.items 返回一個 List 包含 LineItem 物件的 List, 因此需要透過 Collection.flatten() 將之轉為一維的 List. 考慮範例代碼:
  1. printf "Total %d LineItem:\n", invoices*.items.flatten().size()  
  2. invoices*.items.each{  
  3.     printf "%s(%d):\n", it.class, it.size()  
  4.     it.each{  
  5.         printf "\t%sx%d (Total=%d)\n", it.product.name, it.count, it.total()  
  6.     }  
  7. }  
可以得到下面的輸出: 

The interesting part is the comparison of GPath and the corresponding Java code. The GPath 
  1. invoices*.items*.grep{it.total() > 7000}.product.name.flatten()  
leads to the Java equivalent: 
  1. // Java  
  2. private static List getProductNamesWithItemTotal(Invoice[] invoices) {  
  3.     List result = new LinkedList();  
  4.     for (int i = 0; i < invoices.length; i++) {  
  5.         List items = invoices[i].getItems();  
  6.         for (Iterator iter = items.iterator(); iter.hasNext();) {  
  7.             LineItem lineItem = (LineItem) iter.next();  
  8.             if (lineItem.total() > 7000){  
  9.                 result.add(lineItem.getProduct().getName());  
  10.             }  
  11.         }  
  12.     }  
  13.     return result;  
  14. }  
Table 7.4 gives you some metrics about both full versions, comparing lines of code (LOC), number of statements, and complexity in the sense of nesting depth 


There may be ways to slim down the Java version, but the order of magnitude remains: Groovy needs less than 25% of the Java code lines and fewer than 10% of the statements! Writing less code is not just an exercise for its own sake. It also means lower chances of making errors and thus less testing effort. Whereas some new developers think of a good day as one in which they’ve added lots of lines to the codebase, we consider a really good day as one in which we’ve added functionality butremoved lines from the codebase. 

In a lot of languages, less code comes at the expense of clarity. Not so in Groovy. The GPath example is the best proof. It is much easier to read and understand than its Java counterpart. Even the complexity metrics are superior. 

Injecting the spread operator: 
Groovy provides a * spread operator that is connected to the spread-dot operator in that it deals with tearing a list apart. It can be seen as the reverse counterpart of the subscript operator that creates a list from a sequence of comma-separated objects. The spread operator distributes all items of a list to a receiver that can take this sequence. Such a receiver can be a method that takes a sequence of arguments or a list constructor. 

What is this good for? Suppose you have a method that returns multiple results in a list, and your code needs to pass these results to a second method. The spread operator distributes the result values over the second method’s parameters
  1. def getList(){  
  2.     return [1,2,3]  
  3. }  
  4. def sum(a,b,c){  
  5.     return a + b + c  
  6. }  
  7. assert 6 == sum(*getList())  
This allows clever meshing of methods that return and receive multiple values while allowing the receiving method to declare each parameter separately. The distribution with the spread operator also works on ranges and when distributing all items of a list into a second list: 
  1. def range = (1..3)  
  2. assert [0,1,2,3] == [0,*range]  
The same trick can be applied to maps: 
  1. def map = [a:1,b:2]  
  2. assert [a:1, b:2, c:3] == [c:3, *:map]  
The spread operator eliminates the need for boilerplate code that would otherwise be necessary to merge lists, ranges, and maps into the expected format. You will see this in action in section 10.3, where this operator helps implement a user command language for database access. 

Mix-in categories with the use keyword: 
Consider a program that reads two integer values from an external device, adds them together, and writes the result back. Reading and writing are in terms of strings; adding is in terms of integer math. You can’t write: 
  1. write( read() + read() )  
because this would result in calling the plus method on strings and would concatenate the arguments rather than adding them. 

Groovy provides the use method, which allows you to augment a class’s available instance methods using a so-called category. In our example, we can augment the plus method on strings to get the required Perl-like behavior: 
  1. use(StringCalculationCategory) {  
  2.     write( read() + read() )  
  3. }  
A category is a class that contains a set of static methods (called category methods). The use keyword makes each of these methods available on the class of that method’s first argument, as an instance method: 
  1. class StringCalculationCategory {     
  2.   
  3.     static String plus(String self, String operand) {  
  4.         // implementation  
  5.     }          
  6. }  
Because self is the first argument, the plus(operand) method is now available (or overridden) on the String class. Listing 7.24 shows the full example. It implements these requirements with a fallback in case the strings aren’t really integers and a usual concatenation should apply. 
- Listing 7.24 The use keyword for calculation on strings 
  1. class StringCalculationCategory {  
  2.     static def plus(String self, String operand) {  
  3.         try {  
  4.             return self.toInteger() + operand.toInteger()  
  5.         }  
  6.         catch (NumberFormatException fallback){  
  7.             return (self << operand).toString()  
  8.         }  
  9.     }  
  10. }  
  11. use (StringCalculationCategory) {  
  12.     assert 1    == '1' + '0'  
  13.     assert 2    == '1' + '1'  
  14.     assert 'x1' == 'x' + '1'  
  15. }  
The use of a category is limited to the duration of the attached closure and the current thread. The rationale is that such a change should not be globally visible to protect from unintended side effects. Throughout the language basics part of this book, you have seen that Groovy adds new methods to existing classes. The whole GDK is implemented by adding new methods to existing JDK classes. The use method allows any Groovy programmer to use the same strategy in their own code. 

A category can be used for multiple purposes: 
* To provide special-purpose methods, as you have seen with StringCalculationCategory , where the calculation methods have the same receiver class and may override existing behavior. Overriding operator methods is special.
* To provide additional methods on library classes, effectively solving the incomplete library class smell.
* To provide a collection of methods on different receivers that work in combination—for example, a new encryptedWrite method on java.io.OutputStream anddecryptedRead on java.io.InputStream.
* Where Java uses the Decorator pattern, but without the hassle of writing lots of relay methods.
* To split an overly large class into a core class and multiple aspect categories that are used with the core class as needed. Note that use can take any number of category classes.

When a category method is assigned to Object , it is available in all objectsthat is, everywhere. This makes for nice all-purpose methods like logging, printing, persistence, and so on. For example, you already know everything to make that happen for persistence: 
  1. class PersistenceCategory {  
  2.     static void save(Object self) {  
  3.         // whatever you do to store 'self' goes here  
  4.     }  
  5. }  
  6. use (PersistenceCategory) {  
  7.     save()  
  8. }  
Instead of Object , a smaller area of applicability may be of interest, such as all Collection classes or all your business objects if they share a common interface. Note that you can supply as many category classes as you wish as arguments to the use method by comma-separating the classes or supplying them as a list. 
  1. use (ACategory, BCategory, CCategory) {}  
By now, you should have some idea of Groovy’s power features. They are impressive even at first read, but the real appreciation will come when you apply them in your own code. It is worth consciously bearing them in mind early on in your travels with Groovy so that you don’t miss out on some elegant code just because the features and patterns are unfamiliar. Before long, they will become so familiar that you will miss them a lot when you are forced to go back to Java. The good news is that Groovy can easily be used from Java, as we will explore in chapter 11.

沒有留言:

張貼留言

網誌存檔

關於我自己

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