29Jun
2008

Tags:

I've been fascinated with languages like Ruby and Groovy ever since being exposed to their dynamic capabilities. I remember attending a No Fluff Just Stuff conference a few years ago and being awestruck while watching Dave Thomas build a full-blown Rails application within a matter of minutes. Dynamic languages give programmers the ability to write powerful and flexible code that is both readable and concise. But what is it that gives these languages such great power? The secret is their metaprogramming capabilities.

Metaprogramming is the ability of a computer language to manipulate other programs (including itself) to add or create functionality in a dynamic fashion. In Groovy, metaprogramming is especially useful because among other things it gives programmers the ability to easily implement domain specific languages, create builders, or generate mock objects for unit testing. Adding functionality to pre-existing Java or Groovy code can be accomplished by using either the ExpandoMetaClass or Categories.

Note: the best way to test the code examples from this blog entry yourself is to simply copy and paste them into a text file saved with a .groovy extension and then run them from the command line. Running these examples in the GroovyConsole may yield unpredictable results.

The ExpandoMetaClass

The ExpandoMetaClass can be used to dynamically add methods, properties, and constructors to Groovy or Java objects. These tasks can be accomplished by simply assigning a closure to an object's MetaClass. For instance, let's pretend that we want to add a method to java.math.BigDecimal that will convert a BigDecimal (in U.S. Dollars) to its equivalent value in Euros.

import java.text.NumberFormat

BigDecimal.metaClass.inEuros = {
    def EXCHANGE_RATE = 0.634961 // (6/27/2008)
    NumberFormat nf = NumberFormat.getCurrencyInstance(Locale.US)
    nf.setCurrency(Currency.getInstance("EUR"))
    nf.format(delegate * EXCHANGE_RATE)
}

println 2500.00.inEuros()
	

To access BigDecimal's MetaClass we simply use the following syntax BigDecimal.metaClass.inEuros and assign it a closure. Within the closure we set an exchange rate, create a NumberFormat instance, set the currency type to "EUR" (Euros), and multiply the delegate (the BigDecimal we specified) by the exchange rate. Groovy automatically returns the last line of a closure so our call to println 2500.00.inEuros() will yield the following result:

EUR1,587.40

It is also possible to make the code a bit more readable by dropping the parenthesis after the call to inEuros(). This can be accomplished by setting up inEuros as a property on the BigDecimal class instead of as a method:

import java.text.NumberFormat

BigDecimal.metaClass.getInEuros = {->
    def EXCHANGE_RATE = 0.634961 //June 27, 2008
    NumberFormat nf = NumberFormat.getCurrencyInstance(Locale.US)
    nf.setCurrency(Currency.getInstance("EUR"))
    nf.format(delegate * EXCHANGE_RATE)
}

println 2500.00.inEuros
	

Notice that instead of writing inEuros we create a property on the MetaClass by writing getInEuros. This allows us to drop the parenthesis from println 2500.00.inEuros and yields the same result as the method injection technique described above:

EUR1,587.40

For detailed information on the ExpandoMetaClass and all of its uses, be sure to check out the Groovy User Guide.

Categories

Methods, properties, and constructors that are injected into a class via the ExpandoMetaClass are available anywhere within your application. This is a convenient feature if you plan on using the method multiple times but could cause confusion if you only want the changes to take place in isolation. This is where Categories come into play. Categories allow you to inject functionality into a class, but these changes only take affect while calling the method from within the built in use() method's code block.

In order to take advantage of the use() method you must provide it with a "Category". A Category is simply a class with static methods that you wish to inject into your class. Again, the scope of the added functionality is limited to the use() method's calling block. If we take the exchange rate example from above and modify it to be used as a Category the code will be as follows:

import java.text.NumberFormat

class ExchangeRateUtil  {
    def static inEuros(self) {
        def EXCHANGE_RATE = 0.634961 //June 27, 2008
        NumberFormat nf = NumberFormat.getCurrencyInstance(Locale.US)
        nf.setCurrency(Currency.getInstance("EUR"))
        nf.format(self * EXCHANGE_RATE)
    }
}

use(ExchangeRateUtil){
    println 2500.00.inEuros()
}

println 10000.00.inEuros() //throws MissingMethodException
	

I've created a new class called ExchangeRateUtil and created a static method called inEuros() within this class. Notice that method takes a parameter called self which is used to represent the BigDecimal that we specified. The remainder of the code for the conversion is exactly the same.

To use this category we call the use() method and within its block call the inEuros() method the same way we did in the example above. The output will be the same for the call that is made within the use() calling block, but when we try to call the inEuros() method outside of the use() block a MissingMethodException is thrown.

EUR1,587.40
Caught: groovy.lang.MissingMethodException: ...

Conclusion

As you can see, using metaprogramming gives static language programmers a whole new set of tools that can be used to creatively and elegantly arrive at solutions that can otherwise be overly complex in static languages. In my next blog entry, I'll expand upon this current discussion and explain how we can use metaprogramming to not only add functionality to a class, but to actually create it on the fly with no previous knowledge of the methods that will be called on our class.

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

  2. You need to find all instances of “static language” in your article and replace them with the word “Java”. Or maybe learn about higher-order functions and modern type systems and then rewrite it from scratch – OK maybe not from scratch, but you’d probably add a second section called “the pitfalls of metaprogramming” and a third called “how to avoid them”.

    By Greg M on Jul 1, 2008

  3. You’re right. I did mean Java.

    By Justin on Jul 1, 2008

  4. You were very polite to Greg M. I’d have told him if he wanted to read about Scala or Clojure, try not putting the words ‘Groovy’ in his search engine. :P

    One pitfall is of course messing around with methods or properties that can cause a so-called “Action at a Distance” meaning, you co-op’ed a method or property other’s code was not expecting.

    Another danger is ’self’. Best be a BigDecimal. Nonetheless, thanks for the writeup. Writeups trump whining everytime. Got something better to say? Write it up!!! (Heal thy self)

    By Jeff on Jul 6, 2010

Post a Comment