08Jul
2008

Tags:

In my previous post I talked about dynamically adding behavior to Groovy classes using either the ExpandoMetaClass or Categories. These techniques are especially useful if you know which methods you would like to add to your classes prior to actually writing any code. But what if you don't know which methods you will need before code writing time? What if you want to allow yourself the flexibility to call methods arbitrarily without defining their implementation beforehand?

To the average Java programmer (myself included), the idea of generating methods on the fly might seem bizarre at first, but Groovy's built in method interception capabilities allow programmers to easily realize this type of functionality in their Groovy code. In Groovy Metaprogramming - Adding Behavior Dynamically, I discussed how to add a method to the java.math.BigDecimal class to allow for the conversion of U.S. Dollars (USD) into Euros (EUR). What if, however, I wanted to convert from USD to British Pounds (GBP) or to Japanese Yen (JPY)? I could certainly write a method for each of these conversions, but what if there was a better, cleaner, and more flexible way to build in this functionality?

Groovy's methodMissing()

The magic behind Groovy's ability to create methods on the fly comes from the language's built in facilities for intercepting method calls. In particular, one practice for intercepting calls in Groovy is to implement the methodMissing() method on your Groovy classes. Before Groovy throws a MissingMethodException for calls that are made to methods not defined within a class, Groovy first routes the calls through an object's methodMissing() method. This gives programmers a chance to intercept calls to these non-existing methods and define an implementation for them:

import java.text.NumberFormat

def exchangeRates = ['GBP':0.501882, 'EUR':0.630159,
                     'CAD':1.0127, 'JPY':105.87] // (7/2/2008)

BigDecimal.metaClass.methodMissing = { String methodName, args ->
    conversionType = methodName[2..-1]
    conversionRate = exchangeRates[conversionType]

    if(conversionRate){
        NumberFormat nf = NumberFormat.getCurrencyInstance(Locale.US)
        nf.setCurrency(Currency.getInstance(conversionType))

        return nf.format(delegate * conversionRate)
    }

    "No conversion for USD to ${conversionType}"
}

println 2500.00.inGBP()
println 2500.00.inJPY()
println 2500.00.inXYZ()

Notice that in the example above we make calls to the methods: inGBP(), inJPY(), and inXYZ() on the BigDecimal object 2500.00. Also notice however, that we did not actually define any of these methods on the BigDecimal class. Instead we override BigDecimal's methodMissing() method which allows us to intercept calls to these methods (inGBP(), inJPY(), etc.) and create an implementation for them.

Within the methodMissing() method we parse the name of the called method and look up the corresponding conversion rate from the exchangeRate HashMap which stores conversion rate values for various countries. In the future it might be nice if this code called a remote service to actually lookup the exchange rates, but for these examples I'll just keep it simple. If no corresponding conversion rate is found then a message is returned stating that there is "No conversion for USD to <the conversion type specified>" (XYZ in our case).

GBP1,254.70
JPY264,675.00
No conversion for USD to XYZ

Method Caching

One way to improve the performance of the previous example is to cache the implementation of the method calls that we have intercepted. This will allow future calls to these methods to be invoked directly and prevent them from having to be routed through the BigDecimal class's methodMissing() method:

import java.text.NumberFormat

def exchangeRates = ['GBP':0.501882, 'EUR':0.630159,
                     'CAD':1.0127, 'JPY':105.87] // (7/2/2008)

BigDecimal.metaClass.methodMissing = { String methodName, args ->
    println "method missing called"

    def cachedMethod = { Object[] cmArgs ->
        conversionType = methodName[2..-1]
        conversionRate = exchangeRates[conversionType]

        if(conversionRate){
            NumberFormat nf = NumberFormat.getCurrencyInstance(Locale.US)
            nf.setCurrency(Currency.getInstance(conversionType))

            return nf.format(delegate * conversionRate)
        }

        "No conversion for USD to ${conversionType}"
    }

    BigDecimal.metaClass."${methodName}" = cachedMethod

    return cachedMethod(args)
}

println 2500.00.inJPY()
println 2500.00.inGBP()
println 2500.00.inGBP()

In the example above notice that the functionality for the called method is implemented and stored within a closure called cachedMethod. By storing the functionality within a closure we can then assign it to BigDecimal's metaClass so that subsequent method calls are invoked directly.

In the result we can see that the second call to inGBP() does not get routed through BigDecimal's methodMissing():

method missing called
JPY264,675.00
method missing called
GBP1,254.70
GBP1,254.70

Conclusion

It's not too hard to think of ways that the given code samples could be improved. Imagine that instead of assuming all BigDecimal objects were defined in USD, that they could represent any currency type and that our future method calls could actually look something like this:

2500.00.fromEURtoCAD()

The examples given in this blog entry are trivial, but they showcase the power and flexibility that can be gained by using Groovy's metaprogramming capabilities. These metaprogramming techniques give programmers a powerful tool that can be used within their code, but with this power programmers need to show a great deal of caution. Imagine if the following method was called on the BigDecimal object in our examples above:

2500.00.someRandomMethod()

I'm guessing the result would not be desirable. Programmers should always be sure to code defensively and write unit tests when using Groovy's powerful metaprogramming capabilities.

Share and Enjoy:
  • Print
  • Digg
  • del.icio.us
  • Facebook
  • DZone
  • FSDaily
  • Reddit
  • Slashdot
  • StumbleUpon
  • Technorati
  • Twitter
  1. 3 Responses to “Groovy Metaprogramming – Creating Behavior on the Fly”

  2. This is a very nice principle to create DSLs and I like the idea of caching methods.
    Here’s my little comment regarding syntax. I guess, you can replace
    methodName[2..methodName.size()-1]
    with
    methodName[2..-1]

    By Vaclav Pech on Jul 8, 2008

  3. I guess the access to the map ‘exchangeRate’ could be “Groovified” as follows:

    conversionRate = exchangeRates[conversionType]

    Nice article, nonetheless!

    By kodeninja on Jul 8, 2008

  4. Thank you for the great suggestions! I’ll update the code when I get home from work tonight.

    By Justin on Jul 8, 2008

Post a Comment