This page is dedicated to the new Design of the MetaClass.
It seems that we need 2 parts for MetaClass, one for the implementation and one for the user to customize the behavior. The reason for this is, that the customization is done by subclassing the implementation part. This means it is impossible to stay in the specification part (classes from groovy.*) to do a all day thing. Additionally the method signatures must be fixed to not to break code.
Currently we do something like:
- preProcessing might change the arguments, name or call object, or might just do some logging.
- postProcessing might evaluate the return value of the method call, call additional methods, or just do logging
- every part here might end the invokeMethod method, that means super.invokeMethod might not be called
This is very flexible, it allows interception and logging as well as transformation at the same time.
If we want to separate the two cases (runtime MetaClass, user defined MetaClass) in two classes, then one must call the other. As the user part should stay in the specification, it can't know the exact class of the runtime, or which method to call there. This means the user part must be called from the MetaClass in the runtime. Since we are not able to call "super" then we need an additonal Object, that we make the call on:
That's a bit like the continuation passing style.
- A custom MetaClass results in creating a ChainCall object each time an method of that class is invoked.
- The old behavior can be preserved and guranteed
Multiple Behaviors controlled by Class
If we want to split the behavior, then we could have an interface for each of the cases
Maybe it is a bit overkill, since a transformer can act as interceptor, but a logger is surely more lightweight than a interceptor or transformer.
- the common case still requires a ChainCall object each time an method of that class is invoked
- the logging case does not influence the invokation so much
The general disadvantage of the seperation is that we need to associate the custom MetaClass and the runtime MetaClass. Again I see two ways:
- let the runtime get the custom MetaClass:
A null entry in the registry would then mean, that no custom MetaClass is set, in case of GroovyObject, the logic must be changed a little because null here means just no per instance MetaClass. As there is only one runtime MetaClass and several behaviors this means that it is not possible to do
or something alike. Again we could add an interface and make the runtime MetaClass ask the custom MetaClass or to provide an additional set of Properties and Methods at initialization, but then we get another set of interfaces and the runtime MetaClass msut keep track of the changes in case a new custom MetaClass is set.
- let the runtime assign the runtime MetaClass to the custom MetaClass:
So this works much like our ProxyMetaClass. The behavior is much like the old MetaClass, just the signature of invokeMethod for example changes from (Object,String,Object) to (Object,String,Object,ChainCall). It is then no longer possible to invoke a method using invokeMethod directly, it is then only for internal use. Which is no problem, as long as we keep an alternative way for Java as we currently have with InvokerHelper. Multiple chained MetaClasses are also possible this way, the implementation must just keep that in mind and iterate through all of them when making a call. This again is very unlike the ProxyMetaClass we currently have. I would strongly suggest to make MetaClass no interface then and to remove ProxyMetaClass, because the MetaClass then fulfills already the requirements of ProxyMetaClass. Another solution would be to not to let it act as proxy, but to require an additional method:
Where getChainedMetaClass behaves like above, meaning, it returns the next MetaClass, the last MetaClass is then the runtime implementation. This version means the usage is
instead of the old
but it means also, that we can keep the MetaClass interface/class very clean and don't need to add other methods but the methods required for chaining, which we could even make final. Any additional behavior is then controlled by the interfaces I have shown above. This way we could make an MetaClass that intercepts methods, but not properties or fields. Or a version that does logging only on Properties, but does not interfere with normal method invocation. I am aware that this means to have many interface.. 9 then (Interceptor + Transformer + Logger for each of Property, Field and Method) or 6 (if Transformer and Interceptor are unified)
- The custom behavior is unable to tell the difference between a call to super, a call from inside the class, or a normal call from a context outside (could be solved with additional different interfaces)
- implementation and custom behavior are separated
- we can split the runtime MetaClass in a mutable and immutable part and reconstruct the immutable part on demand
- custom MetaClasses becomes more lightweight, since no full initialization step is required for each custom MetaClass and the heavy part can be shared by multiple custom MetaClasses
More efficient custom MetaClasses
Additionally to the traditional Design we could also have a new Design, where we don't take part in the CallChain, but produce a MetaMethod (or property).
which means this method is called in case of a cache miss, and not every time. Returning a MetaMethod we can control if we would like to add the method to the cache or not through the isCachable() method on MetaMethod. We could also change the interface a little and force the call to getMetaMethod even if there is no cache miss. The advantage is, we can still intercept methods, but in a more efficient way, because we don't need to throw a MissingMethodException. And we can use the cache directly if we do not force method selection through this clsss each time, which makes it very fast.
So my proposal is to chain the MetaClasses as shown above, to have a more or less empty MetaClass class which does only the chaining and to have interfaces controlling all aspects of the method invocation (or for properties/fields) like I have shown with logger and transformer or MetaClass2. Having metaClass.queryableMetaClass.getProperties() means to have less methods on MetaClass and since these MetaClasses do normally not add real methods. It would make sense to do so, but metaClass.getProperties() is not really a problem.
- Instead of chaining the MetaClasses like above, we could also have a central MetaClass and just add behavior using interface like the ones I shown (transformer, interceptor...). This would mean to change GroovyObject, because get/setMetaClass becomes meaningless then. If we use a proxy like mechanism, then we end in something that looks almost as the one above, but even more complicated.
- I tried to find a way to keep the call to super, but I was unable to find out how wihtout using a ThreadLocal workaround, which I would like to avoid.