A blog about software development and other software related matters

Blog Archive

Friday, March 6, 2009

MetaClasses can bite!

Groovy has MOP classes built in like the MetaClass class, this class enables (among other things) objects introspection, we could for example use its getProperties() method in order to get the list of all the meta properties of an object and copy their values into another object:


class ParentModel {
def String parentProp

/**
* Copying the current object properties into the other object
*/
def copyInto(other){
this.metaClass.getProperties().findAll{it.getSetter()!=null}.each{metaProp->
if(metaProp.getProperty(this)!=null){
metaProp.setProperty(other,metaProp.getProperty(this))
}
}
other
}
}

The usage is quite simple:

def parent = new ParentModel(parentProp:"parent")
def copiedParent = parent.copyInto(new ParentModel())
assert copiedParent.parentProp.equals(parent.parentProp)

While this code runs perfectly fine with object that have identical classes it has some unexpected side effects when one of the classes derives from another:

class DerivingModel extends ParentModel{
def derivedProp
}

def parent = new ParentModel(parentProp:"parent")
def derived = new DerivingModel(derivedProp:"derived")
def copiedDerived = parent.copyInto(derived)
assert copiedDerived.parentProp.equals(parent.parentProp)
assert copiedDerived.derivedProp.equals("derived")// throws groovy.lang.MissingPropertyException:
// No such property: derived

How can this be? the DerivingModel class has the derivedProp but after the copyInto invocation its missing!
Lets see what the getProperties method actually returns:

def parent = new ParentModel(parentProp:"parent")
parent.metaClass.getProperties().each{println it.name}// prints: class, parentProp, metaClass

It seems that we don't only get the parentProp that we defined but also the metaClass and class properties, this means that in the derived case the copyInto method has set the metaClass and class values of ParentModel into the DerivedModel instance (causing it to lose access to its derivedProp).
The fix is quite simple:

class ParentModel {
def String parentProp
def excludes = ['metaClass']
def copyInto(other){
this.metaClass.getProperties().findAll{it.getSetter()!=null}.each{metaProp->
// we must not set the metaClass of the current object into the other
if(metaProp.getProperty(this)!=null && !excludes.contains(metaProp.name)){
metaProp.setProperty(other,metaProp.getProperty(this))
}
}
other
}
}

There is no need to exclude the class property since it keeps the original even if set.

No comments: