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:
- this
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:
- this.class
The class object reveals available methods with getMethods , so type:
- this.class.methods
- this.class.methods.name
- this.class.methods.name.grep(~/get.*/).sort()
Such an expression is called a GPath. One 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:
- list.property
- list.collect{ item -> item?.property }
- list*.member
- list.collect{ item -> item?.member }
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
- // Set up data structures
- class Invoice {
- List items
- Date date
- }
- class LineItem {
- Product product
- int count
- int total() {return product.dollar * count}
- }
- class Product {
- String name
- def dollar
- }
- // Fill with sample data
- def ulcDate = new Date(107,0,1)
- def ulc = new Product(dollar:1499, name:'ULC')
- def ve = new Product(dollar:499, name:'Visual Editor')
- def invoices = [
- new Invoice(date:ulcDate, items: [
- new LineItem(count:5, product:ulc),
- new LineItem(count:1, product:ve)
- ]),
- new Invoice(date:[107,1,2], items: [
- new LineItem(count:4, product:ve)
- ])
- ]
- // 1) Total for each line item
- assert [5*1499, 499, 4*499] == invoices*.items*.collect{it.total()}.flatten()
- // 2) Query of product names
- assert ['ULC'] == invoices*.items*.grep{it.total() > 7000}.product.name.flatten()
- // 3) Query of invoice date
- def searchDates = invoices.grep{
- it.items.any{it.product == ulc}
- }.date*.toString()
- assert [ulcDate.toString()] == searchDates
Note.
The interesting part is the comparison of GPath and the corresponding Java code. The GPath
- invoices*.items*.grep{it.total() > 7000}.product.name.flatten()
- // Java
- private static List getProductNamesWithItemTotal(Invoice[] invoices) {
- List result = new LinkedList();
- for (int i = 0; i < invoices.length; i++) {
- List items = invoices[i].getItems();
- for (Iterator iter = items.iterator(); iter.hasNext();) {
- LineItem lineItem = (LineItem) iter.next();
- if (lineItem.total() > 7000){
- result.add(lineItem.getProduct().getName());
- }
- }
- }
- return result;
- }
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:
- def getList(){
- return [1,2,3]
- }
- def sum(a,b,c){
- return a + b + c
- }
- assert 6 == sum(*getList())
- def range = (1..3)
- assert [0,1,2,3] == [0,*range]
- def map = [a:1,b:2]
- assert [a:1, b:2, c:3] == [c:3, *:map]
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:
- write( read() + read() )
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:
- use(StringCalculationCategory) {
- write( read() + read() )
- }
- class StringCalculationCategory {
- static String plus(String self, String operand) {
- // implementation
- }
- }
- Listing 7.24 The use keyword for calculation on strings
- class StringCalculationCategory {
- static def plus(String self, String operand) {
- try {
- return self.toInteger() + operand.toInteger()
- }
- catch (NumberFormatException fallback){
- return (self << operand).toString()
- }
- }
- }
- use (StringCalculationCategory) {
- assert 1 == '1' + '0'
- assert 2 == '1' + '1'
- assert 'x1' == 'x' + '1'
- }
A category can be used for multiple purposes:
When a category method is assigned to Object , it is available in all objects—that 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:
- class PersistenceCategory {
- static void save(Object self) {
- // whatever you do to store 'self' goes here
- }
- }
- use (PersistenceCategory) {
- save()
- }
- use (ACategory, BCategory, CCategory) {}
沒有留言:
張貼留言