程式扎記: [ In Action ] Working with closures - The case for closures

標籤

2014年1月26日 星期日

[ In Action ] Working with closures - The case for closures

Preface: 
Closures are important. Very important. They’re arguably one of the most useful features of Groovy—but at the same time they can be a strange concept until you fully understand them. In order to get the best out of Groovy, or to understand anyone else’s Groovy code, you’re going to have to be comfortable with them. Not just “met them once at a wedding” comfortable, but “invite them over for a barbecue on the weekend” comfortable. 

Let’s start with a simple definition of closures, and then we’ll expand on it with an example. closure is a piece of code wrapped up as an object. It acts like a method in that it can take parameters and it can return a value. It’s a normal object in that you can pass a reference to it around just as you can a reference to any other object. Don’t forget that the JVM has no idea you’re running Groovy code, so there’s nothing particularly odd that you could be doing with a closure object. It’s just an object. Groovy provides a very easy way of creating closure objects and enables some very smart behavior. 

If it helps you to think in terms of real-world analogies, consider an envelope with a piece of paper in it. For other objects, the paper might have the values of variables on it: “x=5, y=10” and so on. For a closure, the paper would have a list of instructions. You can give that envelope to someone, and that person might decide to follow the instructions on the piece of paper, or they might give the envelope to someone else. They might decide to follow the instructions lots of times, with a different context each time. For instance, the piece of paper might say, “Send a letter to the person you’re thinking of,” and the person might flip through the pages of their address book thinking of every person listed in it, following the instructions over and over again, once for each contact in that address book. 

The Groovy equivalent of that example would be something like this: 
  1. Closure envelope = { person -> new Letter(person).send() }  
  2. addressBook.each (envelope)  
That’s a fairly long-winded way of going about it, and not idiomatic Groovy, but it shows the distinction between the closure itself (in this case, the value of the envelopevariable) and its use (as a parameter to the each method). Part of what makes closures hard to understand when coming to them for the first time is that they’re usually used in an abbreviated form. Groovy makes them very concise because they’re so frequently used—but that brevity can be detrimental to the learning process. Just for the comparison, here’s the previous code written using the shorthand Groovy provides. When you see this shorthand, it’s often worth mentally separating it out into the longer form: 
  1. addressBook.each { new Letter(it).send() }  
It’s still a method call passing a closure as the single parameter, but that’s all hidden—passing a closure to a method is sufficiently common in Groovy that there are special rules for it. Similarly, if the closure needs to take only a single parameter to work on, Groovy provides a default name— it —so that you don’t need to declare it specifically. That’s how our example ends up so short when we use all the Groovy shortcuts. 

The case for closures 
We’ll look at two particular areas that benefit from closures: performing everyday tasks with collections, and using resources in a safe manner. In these two common situations, you need to be able to perform some logic that is the same for every case and execute arbitrary code to do the actual work. In the case of collections, that code is the body of the iterator; in the case of resource handling, it’s the use of the resource after it’s been acquired and before it’s been released. In general terms, such a mechanism uses a callback to execute the work. Closures are Groovy’s way of providing transparent callback targets as first-class citizens

Using iterators: 
A typical construction in Java code is traversing a collection with an iterator: 
  1. // Java  
  2. for (Iterator iter = collection.iterator(); iter.hasNext();){  
  3.     ItemType item = (ItemType) iter.next();  
  4.     // do something with item  
  5. }  
With a specific implementation of Collection , or an interface such as List , there may be options such as the following: 
  1. // Java  
  2. for (int i=0; i < list.size(); i++){  
  3.     ItemType item = (ItemType) list.get(i);  
  4.     // do something with item  
  5. }  
Java 5 improves the situation with two new features: generics and the enhanced for statement. Generics allow both to be written without casts; indeed, straight-forward iteration can be written in a form that is very similar to the Groovy for loop, using : instead of in : 
  1. // Java 5  
  2. for (ItemType item : list) {  
  3.       // do something with item  
  4. }  
Issue with the enhanced for statement brings us closer to closures, however. Clearly it’s useful to have a for loop that iterates through every item in a collection—otherwise Groovy wouldn’t have it, for starters. (Groovy’s for statement is somewhat broader in scope than Java 5’s—see chapter 6 for more details.) It’s useful, but it’s not everything we could wish for. There are common patterns for why we want to iterate through a collection, such as finding whether a particular condition is met by any element, finding all the elements met by a condition, or transforming each element into another, thereby creating a new collection

It would be madness to have a specialized syntax for all of those patterns. Making a language too smart in a non-extensible way ends up like a road through the jungle—it’s fine when you’re doing something anticipated by the designers, but as soon as you stray off the path, life is tough. So, without direct language support for all those patterns, what’s left? Each of the patterns relies on executing a particular piece of code again and again, once for each element of the collection. Java has no concept of “a particular piece of code” unless it’s buried in a method. That method can be part of an interface implementation, but at that point each piece of code needs its own (possibly anonymous) class, and life gets very messy. 

Groovy uses closures to specify the code to be executed each time and adds the extra methods ( each , find , findAll , collect , and so forthto the collection classes to make them readily available. Those methods aren’t magic, though—they’re simple Groovy, because closures allow the controlling logic (the iterationto be separated from the code to execute for every element. If you find yourself wanting a similar construct that isn’t already covered by Groovy, you can add it easily. 

Separating iteration logic from what to do on each iteration is not the only reason for introducing the closure concept. A second reason that may be even more important is the use of closures when handling resources. 

Handling resources: 
How many times have you seen code that opens a stream but calls close at the end of the method, overlooking the fact that the close statement may never be reached when an exception occurs while processing? So, it needs to be protected with a try-catch block. No—wait—that should be try-finally , or should it? And inside the finallyblock, close can throw another exception that needs to be handled. There are too many details to remember, and so resource handling is often implemented incorrectly. With Groovy’s closure support, you can put that logic in one place and use it like this: 
  1. new File('myfile.txt').eachLine { println it }  
The eachLine method of File now takes care of opening and closing the file input stream properly which prevent a resource leak of file handles. 

Streams are just the most obvious tip of the resource-handling iceberg. Database connections, native handles such as graphic resources, network connections—even your GUI is a resource that needs to be managed (that is, repainted correctly at the right time), and observers and event listeners need to be removed when the time comes, or you end up with a memory leak. 

Resource-handling code is often tested poorly. Projects that measure their test coverage typically struggle to fully cover this area. That is because duplicated, widespread resource handling is difficult to test and eats up precious development time. Testing centralized handlers is easy and requires only a single test. 

Let’s see what resource handling solutions Java provides and why they are not used often, and then we’ll show the corresponding Groovy solutions. 

A common Java approach: use inner classes 
In order to do centralized resource handling, you need to pass resource-using code to the handler. This should sound familiar by now—it’s essentially the same problem we encountered when considering collections: The handler needs to know how to call that code, and therefore it must implement some known interface. In Java, this is frequently implemented by an inner class for two reasons: First, it allows the resource-using code to be close to the calling code (which is often useful for readability); and second, it allows the resource-using code to interact with the context of the calling code, using local variables, calling methods on the relevant object, and so on. 

Anonymous inner classes are almost solely used for this kind of pattern—if Java had closures, it’s possible that anonymous inner classes might never have been invented. The rules and restrictions that come with them (and with plain inner classes) make it obvious what a wart the whole “feature” really is on the skin of what is otherwise an elegant and simple language. As soon as you have to start typing code like MyClass.this.doSomething , you know something is wrong—and that’s aside from the amount of distracting clutter required around your code just to create it in the first place. The interaction with the context of the calling code is limited, with rules such as local variables having to be final in order to be used making life awkward

In some ways, it’s the right approach, but it looks ugly, especially when used often. Java’s limitations get in the way too much to make it an elegant solution. The following example uses a Resource that it gets from a ResourceHandler , which is responsible for its proper construction and destruction. 
  1. // Java  
  2. interface ResourceUser {  
  3.   void use(Resource resource)  
  4. }  
  5. resourceHandler.handle(new ResourceUser(){  
  6.     public void use (Resource resource) {  
  7.         resource.doSomething()  
  8.     }  
  9. });  
The Groovy equivalent of this code reveals all necessary information without any waste: 
  1. resourceHandler.handle { resource -> resource.doSomething() }  
Groovy’s scoping is also significantly more flexible and powerful, while removing the “code mess” that inner classes introduce. 

An alternative Java approach: the Template Method pattern 
Another strategy to centralize resource handling in Java is to do it in a superclass and let the resource-using code live in a subclass. This is the typical implementation of the Template Method [GOF] pattern

The downside here is that you either end up with a proliferation of subclasses or use (maybe anonymousinner subclasses, which brings us back to the drawbacks discussed earlier. It also introduces penalties in terms of code clarity and freedom of implementation, both of which tend to suffer when inheritance is involved. This leads us to take a close look at the dangers of abstraction proliferation

If there were only one interface that could be used for the purpose of passing logic around, like our imaginary ResourceUser interface from the previous example, then things would not be too bad. But in Java there is no such beast—no single ResourceUser interface that serves all purposes. The signature of the callback method useneeds to adapt to the purpose: the number and type of parameters, the number and type of declared exceptions, and the return type. 

Therefore a variety of interfaces has evolved over time: Runnables, Observers, Listeners, Visitors, Comparators, Strategies, Commands, Controllers, and so on. This makes their use more complicated, because with every new interface, there also is a new abstraction or concept that needs to be understood. In comparison, Groovy closures can handle any method signature, and the behavior of the controlling logic may even change depending on the signature of the closure provided to it, as you’ll see later. These two examples of pain-points in Java that can be addressed with closures are just that—examples. If they were the only problems made easier by closures, closures would still be worth having, but reality is much richer. It turns out that closures enable many patterns of programming that would be unthinkable without them. 

Supplement: 
Document > User Guide > Closures 
Working with closures - Declaring closures

沒有留言:

張貼留言

網誌存檔

關於我自己

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