Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: Migrated to Confluence 5.3

Initially developed under the Grailsumbrella and integrated back into Groovy 1.5, ExpandoMetaClass is a very handy way for changing the runtime behavior of your objects and classes, instead of writing full-blow MetaClass classes. Each time, we want to add / change several properties or methods of an existing type, there is too much of a repetition of Type.metaClass.xxx. Take for example this extract of a Unit manipulation DSL dealing with operator overloading:

Code Block
Number.metaClass.multiply = { Amount amount -> amount.times(delegate) }
Number.metaClass.div =      { Amount amount -> amount.inverse().times(delegate) }

Amount.metaClass.div =      { Number factor -> delegate.divide(factor) }
Amount.metaClass.div =      { Amount factor -> delegate.divide(factor) }
Amount.metaClass.multiply = { Number factor -> delegate.times(factor) }
Amount.metaClass.power =    { Number factor -> delegate.pow(factor) }
Amount.metaClass.negative = { -> delegate.opposite() }

The repetition, here, looks obvious. But with the ExpandoMetaClass DSL, we can streamline the code by regrouping the operators per type:

Code Block
Number.metaClass {
    multiply { Amount amount -> amount.times(delegate) }
    div      { Amount amount -> amount.inverse().times(delegate) }
}

Amount.metaClass {
    div <<   { Number factor -> delegate.divide(factor) }
    div <<   { Amount factor -> delegate.divide(factor) }
    multiply { Number factor -> delegate.times(factor) }
    power    { Number factor -> delegate.pow(factor) }
    negative { -> delegate.opposite() }
}

A metaClass() method takes a closure as single argument, containing the various definitions of the methods and properties, instead of repeating the Type.metaClass on each line. When there is just one method of a given name, use the pattern methodName
{ /* closure */ }, but when there are several, you should use the append operator and follow the patten methodName <<
{ /* closure */ }. Static methods can also be added through this mechanism, so instead of the classical approach:

Code Block
// add a fqn() method to Class to get the fully
// qualified name of the class (ie. simply Class#getName)
Class.metaClass.static.fqn = { delegate.name }

assert String.fqn() == "java.lang.String"

You can now do:

Code Block
Class.metaClass {
    'static' {
        fqn { delegate.name }
    }
}

Note here that you have to quote the statickeyword, to avoid this construct to look like a static initializer. For one off method addition, the classical approach is obviously more concise, but when you have several methods to add, the EMC DSL makes sense.

The usual approach for adding properties to existing classes through ExpandoMetaClass is to add a getter and a setter as methods. For instance, say you want to add a method that counts the number of words in a text file, you could try this:

Code Block
File.metaClass.getWordCount = {
    delegate.text.split(/\w/).size()
}

new File('myFile.txt').wordCount

When there is some logic inside the getter, this is certainly the best approach, but when you just want to have new properties holding simple values, through the ExpandoMetaClass DSL, it is possible to define them. In the following example, a lastAccessed property is added to a Car class — each instance will have its property. Whenever a method is called on that car, this property is updated with a newer timestamp.

Code Block
class Car {
    void turnOn() {}
    void drive() {}
    void turnOff() {}
}

Car.metaClass {
    lastAccessed = null
    invokeMethod = { String name, args ->
        def metaMethod = delegate.metaClass.getMetaMethod(name, args)
        if (metaMethod) {
            delegate.lastAccessed = new Date()
            metaMethod.doMethodInvoke(delegate, args)
        } else {
            throw new MissingMethodException(name, delegate.class, args)
        }
    }
}


def car = new Car()
println "Last accessed: ${car.lastAccessed ?: 'Never'}"

car.turnOn()
println "Last accessed: ${car.lastAccessed ?: 'Never'}"

car.drive()
sleep 1000
println "Last accessed: ${car.lastAccessed ?: 'Never'}"

sleep 1000
car.turnOff()

println "Last accessed: ${car.lastAccessed ?: 'Never'}"

In our example, in the DSL, we access that property through the delegate of the closure, with delegate.lastAccessed = new Date(). And we intercept any method call thanks to invokeMethod(), delegating to the original method for the call, and throwing an exception in case the method doesn't exist. Later on, you can see by executing this script that lastAccessed is updated as soon as we call a method on our instance.