В этой статье рассказывается о том, как перенести повторное использование на следующий уровень с помощью функции языка закрытия Groovy, которая помогает повторно использовать, перехватывать вызовы методов и даже заменять методы во время выполнения. Затем мы обсудим истинную природу замыканий.
Замыкание — это исполняемый блок кода, похожий на анонимный метод, но его можно рассматривать как ссылку на объект. Он следует многим тем же правилам, что и анонимный внутренний класс в Java. На протяжении всей этой статьи будет раскрыта истинная природа.
В прошлой статье мы использовали invokeMethod и вызов идиоматического метода со строками для получения другого уровня повторного использования. В этой статье мы сделаем то же самое, но с Groovy замыканиями.
Использование замыканий важно для понимания Groovy, поскольку GDK широко использует замыкания. С Groovy замыканиями легко перейти на следующий уровень повторного использования. GDK расшифровывается как Groovy Development Kit и представляет собой способ расширения базовых классов Java с помощью возможностей языка Groovy в Groovy (GDK даже расширяет конечные классы, такие как String и Integer).
В последней статье мы показали простой пример, который представлял собой класс Computer, использующий шаблон проектирования Observer / Observable для уведомления заинтересованных сторон о том, что произошло событие аренды.
На случай, если вы не читали последнюю статью, класс Computer использует / реализует события Java следующим образом:
RentableEvent, расширяющий java.util.EventObject
public class RentableEvent extends EventObject{
RentableEvent (Computer source) {
super(source)
}
}
RentListener, который расширяет java.util.EventListener
interface RentListener extends EventListener {
void rented(RentableEvent re)
void returned(RentableEvent re)
void overdue(RentableEvent re)
}
Groovy еще нет вкусностей. Выше приведена стандартная Java около 1997 года. Слушатель определяет три события: когда компьютер арендуется, когда компьютер возвращается и когда компьютер был возвращен, но просрочен.
Тогда в классе Computer у нас будет следующий код для запуска событий:
Компьютерный класс, который можно сдавать в аренду
public class Computer implements Serializable, Rentable {
...
/** rentableListeners List of RentListener's property that holds listeners. */
List rentableListeners = []
/** Rent the computer. */
void rent(int days=5) {
/* Throws an IllegalStateException if the computer was already rented. */
if (rented) {
throw new IllegalStateException("You can't rent a computer twice")
}
/* Set the days property, startRent and the rented property accordingly. */
this.days = days
rented = true
startRent = Calendar.getInstance()
/* Fire the rent event. */
fireRentedEvent()
}
void endRent(Calendar returnedOn=Calendar.getInstance()){
/* Determine if the due date is before the returned date. */
Date dueDate = startRent.time + days
Date actualReturnDate = returnedOn.time
/* Fire the appropiate event based on the overdue status. */
if (dueDate.before (actualReturnDate)) {
fireOverdueEvent()
} else {
fireReturnedEvent()
}
rented = false
}
/* Fire rented event. */
public void fireRentedEvent() {
RentableEvent event = new RentableEvent(this)
rentableListeners.each {RentListener listener -> listener.rented(event) }
}
/* Fire overdue event. */
public void fireOverdueEvent() {
RentableEvent event = new RentableEvent(this)
rentableListeners.each {RentListener listener -> listener.overdue(event) }
}
/* Fire returned event. */
public void fireReturnedEvent() {
RentableEvent event = new RentableEvent(this)
rentableListeners.each {RentListener listener -> listener.returned(event) }
}
void addRentableListener(RentListener listener) {
rentableListeners << listener
}
void removeRentableListener(RentListener listener) {
rentableListeners.remove(listener)
}
}
NOTE: This Computer example is adapted from our Groovy course and the original was written by Alex Kotovich and I have heavily modified it and re-purposed it a few times.
Notice that the above fires three types of events as follows: fireRentedEvent, fireOverdueEvent, and fireReturnedEvent.
Review: For those new to Groovy, to fire an event, the above code uses Groovy’s GDK method for collections called each, which will invoke the block (called a closure) for each iteration (each item) in the collection of listeners. Then inside of the block (again called a closure), we call the appropriate method to notify the listener of the event.
One thing you will notice about fireRentedEvent, fireOverdueEvent, and fireReturnedEvent is that they are nearly the same. Thus the issue is we have three methods that are nearly the same. Essentially they break the Don’t-repeat-yourself (DRY) principle. Let’s resolve this issue using Groovy closures.
In Groovy, it is easy to make the listener method that gets called a parameter by using a closure. Notice the use of a closure in the fireRentEvent as follows:
Using closures to get rid of three code blocks
public void fireRentEvent(Closure closure) {
assert closure: "Closure should not be null"
RentableEvent event = new RentableEvent(this)
rentableListeners.each {RentListener listener ->
closure(event, listener)
}
}
private void fireRentedEvent() {
Closure closure = {RentableEvent event, RentListener listener -> listener.rented(event) }
fireRentEvent(closure)
}
/* Fire overdue event. */
private void fireOverdueEvent() {
fireRentEvent({RentableEvent event, RentListener listener -> listener.overdue(event) })
}
/* Fire returned event. */
private void fireReturnedEvent() {
fireRentEvent({RentableEvent event, RentListener listener -> listener.returned(event) })
}
The fireRentedEvent calls the fireRentEvent passing it a closure.
Breaking down the closure example
Let’s dissect the above example a bit. To define a closure we use the following syntax:
Closure closure = {RentableEvent event, RentListener listener -> listener.rented(event) }
The above looks like we are assigning a block of code to a object reference and this is exactly what we are doing. Just like Groovy has literal support for defining a Map, List and a Regex. Groovy has literal support for defining reusable blocks of code that can be passed around like object references a.k.a. closures.
If you think of a closure as an anonymous method then you can think of the following:
RentableEvent event, RentListener listener ->
as defining the arguments to that method. Then the body of the method is after the ->, i.e.,
-> listener.rented(event)
Combined, the above defines that closure that we can later pass to the fireRentEvent as follows:
fireRentEvent(closure)
Then this block of code gets used inside of the fireRentEvent as follows:
public void fireRentEvent(Closure closure) {
assert closure: "Closure should not be null"
RentableEvent event = new RentableEvent(this)
rentableListeners.each {RentListener listener ->
closure(event, listener)
}
}
We got rid of the three for loops by using a closure.
Making it more groovy
Since these are internal methods for firing events and not public APIs, it might make sense to reduce the code size by getting rid of the strong typing. I tend to always use strong typing unless I need dynamic typing to get more reuse. To me strong typing aids in the readability of the code. However, when using closures and such, it is often easier to use dynamic typing as the object types you are dealing with are easily discernible by context. It is a matter of taste and style so without being prescriptive let’s use dynamic typing as follows:
/** Fire rented event. */
public void fireRentEvent(closure = {event, listener -> listener.rented(event) } ) {
rentableListeners.each {RentListener listener ->
closure(new RentableEvent(this), listener)
}
}
/** Fire overdue event. */
private void fireOverdueEvent() {
fireRentEvent({event, listener -> listener.overdue(event) })
}
/** Fire returned event. */
private void fireReturnedEvent() {
fireRentEvent({event, listener -> listener.returned(event) })
}
Notice that the code is much shorter and we got rid of one of the methods. Since the code is shorter, it feels more comfortable using a default closure parameter. Now by default the fireRentEvent calls listener.rented by using a Groovy default argument construct.
Note that there is still strong typing in the body of the fireRentEvent so you can still see what event and listener you are dealing with but the closure as parameter construct uses the dynamically typed approach to keep it short. It is arguably a good combination of strong vs. dynamic to make the code more readable yet still get a lot of reuse.
More closures
Another option is to get rid of of the helper methods all together, make the closures members and use them as arguments to the fireRentEvent when calling it.
/** Fire rent event. */
public void fireRentEvent(closure = fireRentedEvent) {
rentableListeners.each {RentListener listener ->
closure(new RentableEvent(this), listener)
}
}
/** Fire rented event */
def fireRentedEvent = {event, listener -> listener.rented(event) }
/** Fire overdue event. */
def fireOverdueEvent = {event, listener -> listener.overdue(event) }
/** Fire returned event. */
def fireReturnedEvent = {event, listener -> listener.returned(event) }
The definition of a closure versus a method is somewhat blurred anyway. To see what I am talking about try this in the GroovyConsole app:
/* Define a closure. */
Closure helloWorldClosure = {String message, String person -> println "Hello ${person} : ${message}"}
/* Use the closure we defined. */
helloWorldClosure("How are you?", "Rick")
/* Define a method. */
def helloWorld(String message, String person) {
println "Hi ${person} : ${message}"
}
/* Use the method we just defined. See the difference? I don't. */
helloWorld("How the hell are you?", "Rick")
/* Assign the the new method to closure variable. */
helloWorldClosure = this.&helloWorld
/* Now use the closure/method. Methods and closures are first class object. */
helloWorldClosure("Hiya howzit been?", "Rick")
/* Check this out. What is this doing? */
helloWorldClosure = System.out.&println
helloWorldClosure("Hello Rick")
In the above we define a closure called helloWorldClosure and then invoke it. Then we define a nearly equivalent method called helloWorld and invoke it in the same manner. Then we assign the closure reference the method using this.&helloWorld. In this case, this refers to the Script that the GroovyConsole created. In Groovy if a file does not define a class, the the name of the file becomes the name of the class that Groovy generates on your behalf, but in the GroovyConsole there is no file so the name of class becomes things like Script12, Script13 etc. Thus, this refers to Script12 class (or whatever number the GroovyConsole is on). Then we take the closure now referencing the method and invoke it the same way. And we do this to show the similarities of closures and methods. To replace a method at runtime, one merely could assign it a closure after all Groovy is a dynamic language (could != should).
In the last section of the article we will try to un-blur the distinction between methods and closures.
Currying
If we are going to go this far opening up to closures, we would be remiss not to tap on the curry door. Let’s tap currying with this example.
/** Fire rent event. */
public void fireRentEvent(closure = fireRentedEvent) {
rentableListeners.each {RentListener listener ->
closure(listener)
}
}
/** Fire rented event */
def fireRentedEvent = ({event, listener -> listener.rented(event) }).curry(new RentableEvent(this))
/** Fire overdue event. */
def fireOverdueEvent = ({event, listener -> listener.overdue(event) }).curry(new RentableEvent(this))
/** Fire returned event. */
def fireReturnedEvent = {event, listener -> listener.returned(event) }.curry(new RentableEvent(this))
In our example, the RentableEvent only holds the source of the rentable event which does not change in the context of an instance of the Computer. It would be nice if we could store the RentableEvent as part of the closure and leave it off of calls to the closure. To curry with a closure is to have the closure remember a parameter. The curry method of the Closure object returns a new instance of a Closure that remember (from left to right) the arguments that were added to the closure. Thus the fireRentedEvent remembers its first argument is a RentableArgument because it is a the results of the call to closure.curry(new RentableEvent(this)).
Standard disclaimer
I would be remiss not to add the following disclaimer. Now granted this article is instructive; therefore, the example it uses is a bit contrived. However there are times when you have very similar blocks of code that do nearly the same thing except for the method that is getting called and yes I know you could rewrite the above much smaller as follows:
/* Fire returned event. */
public void fireReturnedEvent() {
rentableListeners.each {it.returned([this] as RentableEvent) }
}
And the above is a valid point. However, the point of the article is how do you achieve reuse when you have very similar blocks of code (in this case methods) that do nearly the same thing except invoke different methods. In Java, it becomes a bit harder to get reuse. You end up writing inner classes to pass which method gets called or duplicating code or if you are dealing with a hierarchy of objects, you might implement the visitor design pattern. In Groovy, you can just use closures (this does not preclude using a Visitor design pattern but merely gives you the choice).
Case study of method references to aid in resue
Earlier in the discussion of what is a closure we introduced the subject of method pointer. In groovy a method is a true object and you can get a reference to it using the this.&methodName syntax. Recently I wrote a code generator for a client using Groovy. I had a piece of code that used meta data from a JDBC connect. The JDBC connection metaData object has two methods to get information about keys in a table, getExportedKeys and getImportedKeys. At first I had two methods that were nearly structurally identical. Then I was able to take reuse to a new level by letting the method itself be a parameter — note processKeys calls the overloaded processKeys twice passing the connection.metaData.&getExportedKeys on the first pass and the second connection.metaData.&getImportedKeys as follows:
/**
* Process import keys and export keys
*/
def processKeys() {
tables.each {Table table ->
processKeys(table, connection.metaData.&getExportedKeys, table.exportedKeys, false)
processKeys(table, connection.metaData.&getImportedKeys, table.importedKeys, true)
}
}
def processKeys(Table table, getKeys, List keyList, boolean imported) {
if (debug) println "processing keys for table ${table.name} imported=${imported}"
jdbcUtils.iterate getKeys(catalog, schema, table.name),
{ResultSet resultSet ->
... (more code)
}
... (more code)
if (debug) println "\n"
}
We could have achieved similar reuse with closures, or even methodName/invokeMethod approach from the last article. This is just another way to stay DRY with Groovy and its an option because methods are first class object.
Advanced: Changing methods at runtime
To show how closures and methods are similar, let’s change some methods with closures at runtime.
You can change methods at runtime. For example let’s say we want to redirect println methods to a Swing JTextArea. We can create a printlnClosure and then switch out the println method of each of the utility classes at runtime.
Thus, when the classes run from the command line (no swing GUI), they print to the standard console, but when we run it in our swing application, we can just switch the implementation of the println method out as follows using the metaClass from Groovy:
Closure printlnClosure = {String message ->
... code to print to a TextArea
}
JdbcUtils.metaClass.println = printlnClosure
DataBaseMetaDataReader.metaClass.println = printlnClosure
JavaModelGenerator.metaClass.println = printlnClosure
XMLPersister.metaClass.println = printlnClosure
Note: JdbcUtils, DataBaseMetaDataReader, JavaModelGenerator are all Groovy classes that I created for a utility I recently wrote.
By using metaClass.println = printlnClosure we effectively switch out the method at runtime. Note: we have to do this before instances of these classes are created. You can can also switch out methods on instances. Thus when we create a new instance of DataBaseMetaDataReader its println is effectively the printlnClosure.
Advanced: Intercepting method calls
You can provide AOP like functionality by combining the ability to switch methods at runtime with the special Groovy method invokeMethod which is a method that gets called whenever a method gets invoked on an object. Here is an example of intercepting method calls:
Closure logClosure = {String methodName, methodArgs ->
def validMethod = delegate.metaClass.getMetaMethod(methodName, methodArgs)
if (validMethod == null) {
return delegate.metaClass.invokeMissingMethod(delegate, methodName, methodArgs)
} else if (validMethod.name == "println") {
return validMethod.invoke(delegate, methodArgs)
}
println "Running ${methodName}(${methodArgs})"
def result = validMethod.invoke(delegate, methodArgs)
println "Completed ${methodName}(${methodArgs})"
result
}
Side bar: Notice that logClosure looks up the method by name and then checks to see if the reference is null. I choose to use the syntax validMethod==null to highlight that we are dealing with an Object reference; however, I could have relied on Groovy truth and written !validMethod. In Groovy a null reference is false. Groovy truth in general is preferred.
Here are the three classes that we decorate as follows:
DataBaseMetaDataReader.metaClass.invokeMethod = logClosure
JavaModelGenerator.metaClass.invokeMethod = logClosure
JPACodeGenerator.metaClass.invokeMethod = logClosure
Now when new instances of DataBaseMetaDataReader, JavaModelGenerator or JPACodeGenerator get created they will be decorated with our AOP-like logClosure. For example, logClosure will print out «Running someMethod(1,2,3)» and «Completed someMethod(1,2,3)» when a method called someMethod gets called with the arguments 1,2,3.
In the logClosure you may have notice the reference to delegate. And if you were like me, you are wondering what is the delegate. Well like classes have a «this» which refers to the current instance, Closure have a «delegate» which refers to the instance of the class that is being decorated. Hopefully this little sample script should clear things up a bit:
samplescript.groovy that uses modified logClosure
Closure logClosure = {String methodName, methodArgs ->
println "Delegate class ${delegate.class.name}"
println "Class ${this.class.name}"
println "Owner ${owner.class.name}"
def validMethod = delegate.metaClass.getMetaMethod(methodName, methodArgs)
if (validMethod == null) {
return delegate.metaClass.invokeMissingMethod(delegate, methodName, methodArgs)
} else if (validMethod.name == "println") {
return validMethod.invoke(delegate, methodArgs)
}
println "Running ${methodName}(${methodArgs})"
def result = validMethod.invoke(delegate, methodArgs)
println "Completed ${methodName}(${methodArgs})"
result
}
class Employee {
def changeMe(hi) {
println "Hello world $hi"
}
}
println "Delegate before assign ${logClosure.delegate.class.name}"
Employee.metaClass.invokeMethod = logClosure
println "Delegate after assign ${logClosure.delegate.class.name}"
Employee emp = new Employee()
emp.changeMe("Rick")
Notice that we now print the class name of the delegate and the this. Now compare the sample code listing to the output:
richard-hightowers-macbook-pro:~ richardhightower$ groovy samplescript.groovy
Delegate before assign samplescript
Delegate after assign samplescript
Delegate class Employee
Class samplescript
Owner samplescript
Running changeMe({"Rick"})
Delegate class Employee
Class samplescript
Owner samplescript
Hello world Rick
Completed changeMe({"Rick"})
Notice that the delegate is Employee (as expected) when the logClosure is running.
Closures Redux
I sent this out for review. It felt like it was missing more of a description of closures and that I left off with just describing closures as nothing but anonymous method, and even I know better than this. But I was not sure how to discuss more of closures nature.
Then Ken Sipe wrote me and confirmed my fear. Ken just wrote an article on modern functional programming for No Fluff Just Stuff series monthly newsletter. He writes:
I just recently finished an article on functional programming which
includes the details I wanted to communicate to you on closures.
(btw… I would appreciate any feedback you might have on it as well)
Closures somehow became synonymous with anonymous functions, which
isn’t necessarily the case. You mention in the article that the
definition of closure vs. method are somewhat blurred. I have to
admit I thought that as well not that long ago. However as I’ve
traveled a little down the functional road along with a little
research. It has become more clear. … Closures are a specialized method.
They provide the ability within a language to store or refer to variables
which when looked at from a scope perspective are clearly out of
scope, yet the variables live on. This is a point of closure.
This is certainly the case. By the way, Ken Sipe wrote an excellent article on modern functional programming and it reinforces my desire to learn more about the Clojure language. I would publish information about where to get it, but that information is not available yet. When it becomes available, I will add it to the comments.
If you didn’t know before, closures may seem like just anonymous methods but there is more to them than this. The name closure comes from the fact that they capture the local variables of the outer scope which is best illustrated with this non-groovy example which is from Ken Sipe’s article:
Function powerFunctionFactory(int power) {
int powerFunction(int base) {
return pow(base, power);
}
return powerFunction;
}
Function square = powerFunctionFactory (2);
square(3); // returns 9
Function cube = powerFunctionFactory (3);
cube(3); // returns 27
«Technically closures are dynamically allocated data structures containing a code pointer, pointing to a fixed piece of code computing a function result and an environment of bound variables. A closure is used to associate a function with a set of “private” variables.» —Ken Sipe, NFJS Newletter on Modern Functional Programming Languages
The above code defines an outer function called powerFunctionFactory and an inner function powerFunction. The outer function, creates instances of the inner function. The inner function is configured with the current power which it gets from the outer function; the inner function encloses the local variables from the outer function. Thus we are able to define square and cube functions from the powerFunctionFactory. In essence, powerFunctionFactory makes powerFunction configurable. The powerFunction is a block of code that has access to the local variables of powerFunctionFactory even when powerFunctionFactory variables go out of scope.
Groovy has the same concept and it can be expressed with closures as follows:
Closure powerClosureFactory(int power) {
Closure powerClosure = { int base ->
return Math.pow(power, base)
}
return powerClosure
}
Closure square = powerClosureFactory(2)
square(2)
Closure cube = powerClosureFactory(3)
cube(3)
The above code defines a method called powerClosureFactory and a closure called powerClosure. The outer methods, creates instances of the inner closure. The inner powerClosure is configured with the current power which it gets from the outer method; the inner closure encloses the local variables from the outer method. Thus we are able to define square and cube closures like methods from the powerClosureFactory. In essence, powerClosureFactory makes the powerClosure configurable. The powerClosureFactory is a block of code that has access to the local variables of powerClosureFactory even when powerClosureFactory variables go out of scope.
Here would be a groovier way to write the above:
def powerClosureFactory(power) {
{int base -> Math.pow(power, base)}
}
def square = powerClosureFactory(2)
square(2)
def cube = powerClosureFactory(3)
cube(3)
Except of course the Groovy version is much smaller and prettier (eyes of the beholder.). Note that return is not needed. The last statement is automatically the return, thus, {int base -> Math.pow(power, base)} defines a closure and returns it in one line of code. The closure body returns the results of Math.pow (again without a return).
In Ken Sipe’s article, he went on to describe how F# can map values from one list to a new list as follows:
let squares2 = List.map (fun x -> x*x) [1; 2; 3; 4]
The above takes the list (with 1, 2, 3, 4 in it) and maps it to a new list of squares with the results 1, 4, 9, 16 in it. Groovy has the same concept but like Python it calls it collect as follows:
def squares = [1,2,3,4].collect{x -> x*x}
The collect method takes a closure as an argument and it maps the one list into a new list of squares and just like the F# version shown earlier it creates a new list with the values [1, 4, 9, 16]. By the way, F# is a functional programming language that runs in the .Net environment and is based on Objective Camel. Now since we are dealing with Groovy, when you only have one argument being passed to your closure you don’t have to name it. By default the name of the first argument is «it» demonstrated as follows:
def squares = [1,2,3,4].collect{it*it}
The idea of this section was to introduce that closures were more then just anonymous methods. For more details on functional programming, I recommend Ken Sipe’s article on Functional Programming.
Conclusion
In this short article, we showed how you can push reuse to a new level and stay extremely DRY by using Groovy’s closures. We also showed how to achieve the same by using method references and how methods and closures are similar. Then we went on to show how to replace methods at runtime using closures and perform AOP like programming using closures. And then for completeness, we showed that closure were more than just anonymous methods.
Note that this article is not prescriptive. It does not say you should use method references or closure, merely, that these are viable options and weapons in your development arsenal.
Special thanks Andres Almiray, Ken Sipe and Alex Kotovich for their help. Ken and Andres provided valuable insight.
If you liked this article and you are considering learning more about Groovy, please consider ArcMind’s Groovy course: Groovy training or our Grails course: Grails Training. Also I plan on adding Groovy language support to the soon to be renamed Crank project.
You can follow Rick at http://twitter.com/RickHigh and http://www.jroller.com/RickHigh/