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. A 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:
- Closure envelope = { person -> new Letter(person).send() }
- addressBook.each (envelope)
- addressBook.each { new Letter(it).send() }
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:
- // Java
- for (Iterator iter = collection.iterator(); iter.hasNext();){
- ItemType item = (ItemType) iter.next();
- // do something with item
- }
- // Java
- for (int i=0; i < list.size(); i++){
- ItemType item = (ItemType) list.get(i);
- // do something with item
- }
- // Java 5
- for (ItemType item : list) {
- // do something with item
- }
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 forth) to 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 iteration) to 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:
- new File('myfile.txt').eachLine { println it }
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.
- // Java
- interface ResourceUser {
- void use(Resource resource)
- }
- resourceHandler.handle(new ResourceUser(){
- public void use (Resource resource) {
- resource.doSomething()
- }
- });
- resourceHandler.handle { resource -> resource.doSomething() }
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 anonymous) inner 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
沒有留言:
張貼留言