So far Groovy allow per instance meta classes only for objects, which implements GroovyObject interface (like any class defined in Groovy). It can be illustrated by following simple test case:
// for java.lang.String
"test string".metaClass.toString = { "modified string" }
assertEquals( "modified string", "test string".toString() )
// but any other string will have the same behavior
assertEquals( "modified string", "any other string".toString() )
At the same time
// for GroovyObject
class MyBean {
String toString () { "bean" }
}
def bean = new MyBean ()
def emc = new ExpandoMetaClass(MyBean,false,true)
emc.toString = { -> "modified bean" }
bean.metaClass = emc
assertEquals("modified bean", bean.toString())
// but any other bean will keep default behavior
assertEquals("bean", new MyBean().toString())
// if we want to modify behavior of all MyBean objects
MyBean.metaClass = emc
assertEquals("modified bean", new MyBean().toString())
Notice that we use different technique to add methods to Java and Groovy objects. As getMetaClass () for Java object always returns the same meta class we can use .metaClass notation, each GroovyObject has it is own meta class (of course, it is the same object by default) so we act differently depending on what we want to modify (whole class or just one Object)
Now I have patch which solves all these problems.
First of all we can assigned per instance meta class for any object. Of course, for Java objects it kept in WeakHashMap which involves some performance penalties. But now our first sample looks much more natural
// for java.lang.String
"test string".metaClass.toString = { "modified string" }
assertEquals( "modified string", "test string".toString() )
// but any other string will have natural behavior
assertEquals( "any other string", "any other string".toString() )
Groovy objects also has benefits – we don’t need to create ExpandoMetaClass manually.
def bean = new MyBean ()
bean.metaClass.toString = { -> "modified bean" }
assertEquals("modified bean", bean.toString())
// but any other bean will keep default behavior
assertEquals("bean", new MyBean().toString())
// if we want to modify behavior of all MyBean objects
MyBean.metaClass = bean.metaClass
assertEquals("modified bean", new MyBean().toString())
But now, when we have unified behavior for Java and Groovy objects we can use simply and powerful ExpandoMetaClass DSL. Here is example
// add behavior to meta class for Integer
Integer.metaClass {
// block of static methods
'static' {
fib { Number n ->
n.fib ()
}
// we can use both = or << if needed
unusedStatic << { -> }
}
// one more syntax for static methods
static.unusedStatic2 = { -> }
// property definition
ZERO = 0
// method definition
fib { ->
def n = delegate
if (n == ZERO)
return 1;
if (n == 1)
return 1
else
return fib(n-1) + fib(n-2)
}
// of course we can use both = or << if needed as well
unusedMethod << { -> }
}
// and now we have it
assertEquals( 3, 3.fib())
// but we can also modify meta class for particular number
4.metaClass {
fib { ->
10
}
}
// and it works!
assertEquals( 13, Integer.fib(5))
You know what… I like it. And thanks a lot for Graeme for inspirations and advices while I worked on that. Hope it will be accepted well and commited to the trunk.
[…] dynamic stateless mixins Last week I wrote about per-instance meta-classes and ExpandoMetaClass DSL. At that point it was just prototyped code in my working copy and I was not even sure if it will […]