craigaspinall.com

03 Mar 2011 - Extending classses at runtime with Groovy

I gave an introduction to Groovy at our local JUG this week and didn’t quite manage to make it through all the material I wanted to cover, so I decided to add the missing content here! The part I missed out in the meetup was how to extend a class at runtime via it’s metaClass.

If you add a named closure to the metaClass of an existing class, then it essentially becomes available as a method on that class. In this example, I’ve added a containsOnly(Collection anotherCollection) method to the Collection class, which returns true if two collections have the same content but not necessarily in the same order.

Collection.metaClass.containsOnly = { Collection otherCollection ->
    delegate.containsAll(otherCollection) && delegate.size() == otherCollection.size()
}

a = [1, 2, 3, 4]
b = [2, 4, 3, 1]
c = [2, 4, 3, 5]
assert a.containsOnly(b)
assert !b.containsOnly(c)

delegate is an implicit argument referring to the object on which the containsOnly() method is being called.

In the following case, I have extended the Collection class again to add a choose(int numberOfElements) method, which selects a supplied number of elements from the collection, chosen at random. Note that the return keyword is optional, I’ve used it here for clarity.

Collection.metaClass.choose = { int numberOfElements ->
    if (delegate.size() <= numberOfElements) {
        return delegate
    } else {
        List previouslyUsed = []
        List chosen = []
        while (chosen.size() < numberOfElements) {
            int index = new Random().nextInt(delegate.size())
            if (!previouslyUsed.contains(index)) {
                chosen << delegate[index]
                previouslyUsed << index
            }
        }
        return chosen
    }
}

a = [1,2,3,4,5,6,7,8,9,10]
one = a.choose(1)
assert one.size() == 1
assert a.containsAll(one)
five = a.choose(5)
assert five.size() == 5
assert a.containsAll(five)
twelve = a.choose(12) // should only return 10!
assert twelve.size() == 10
assert a.containsAll(twelve)

My final example extends the String class so that you can easily cast it to a Date instance using a fixed conversion format. Groovy uses this syntax for casting (which is much nicer than Java):

"08/08/1988" as Date

Under the hood, Groovy calls the asType(Class targetType) method to perform the conversion, and that already supports casting to a number of different types. To add Date casting I had to replace the existing method definition. So that I didn’t lose the original functionality, I captured the original method and delegated to it if we’re not trying to cast to a Date.

def oldAsType = String.metaClass.getMetaMethod("asType", [Class] as Class[])
String.metaClass.asType = { Class targetType ->
    if (targetType == Date.class) {
        Date.parse("dd/MM/yyyy", delegate)
    } else {
        oldAsType.invoke(delegate, targetType)
    }
}
// Now we can call...
"08/08/1988" as Date

Newer Posts

Older Posts

This work is licensed under a Creative Commons Attribution 3.0 Unported License.

Copyright ©Craig Aspinall 2011