程式扎記: [ Groovy Doc ] ExpandoMetaClass - Domain-Specific Language

標籤

2015年1月11日 星期日

[ Groovy Doc ] ExpandoMetaClass - Domain-Specific Language

Source From Here 
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: 
  1. Number.metaClass.multiply = { Amount amount -> amount.times(delegate) }  
  2. Number.metaClass.div =      { Amount amount -> amount.inverse().times(delegate) }  
  3.   
  4. Amount.metaClass.div =      { Number factor -> delegate.divide(factor) }  
  5. Amount.metaClass.div =      { Amount factor -> delegate.divide(factor) }  
  6. Amount.metaClass.multiply = { Number factor -> delegate.times(factor) }  
  7. Amount.metaClass.power =    { Number factor -> delegate.pow(factor) }  
  8. 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: 
  1. Number.metaClass {  
  2.     multiply { Amount amount -> amount.times(delegate) }  
  3.     div      { Amount amount -> amount.inverse().times(delegate) }  
  4. }  
  5.   
  6. Amount.metaClass {  
  7.     div <<   { Number factor -> delegate.divide(factor) }  
  8.     div <<   { Amount factor -> delegate.divide(factor) }  
  9.     multiply { Number factor -> delegate.times(factor) }  
  10.     power    { Number factor -> delegate.pow(factor) }  
  11.     negative { -> delegate.opposite() }  
  12. }  
metaClass() method takes a closure as single argument, containing the various definitions of the methods and properties, instead of repeating theType.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 << 

Static methods can also be added through this mechanism, so instead of the classical approach: 
  1. // add a fqn() method to Class to get the fully  
  2. // qualified name of the class (ie. simply Class#getName)  
  3. Class.metaClass.static.fqn = { delegate.name }  
  4.   
  5. assert String.fqn() == "java.lang.String"  
You can now do: 
  1. Class.metaClass {  
  2.     'static' {  
  3.         fqn { delegate.name }  
  4.     }  
  5. }  
Note here that you have to quote the static keyword, 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: 
  1. File.metaClass.getWordCount = {  
  2.     delegate.text.split(/\w/).size()  
  3. }  
  4.   
  5. 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 theExpandoMetaClass 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. 
  1. class Car {  
  2.     void turnOn() {}  
  3.     void drive() {}  
  4.     void turnOff() {}  
  5. }  
  6.   
  7. Car.metaClass {  
  8.     lastAccessed = null  
  9.     invokeMethod = { String name, args ->  
  10.         def metaMethod = delegate.metaClass.getMetaMethod(name, args)  
  11.         if (metaMethod) {  
  12.             delegate.lastAccessed = new Date()  
  13.             metaMethod.doMethodInvoke(delegate, args)  
  14.         } else {  
  15.             throw new MissingMethodException(name, delegate.class, args)  
  16.         }  
  17.     }  
  18. }  
  19.   
  20.   
  21. def car = new Car()  
  22. println "Last accessed: ${car.lastAccessed ?: 'Never'}"  
  23.   
  24. car.turnOn()  
  25. println "Last accessed: ${car.lastAccessed ?: 'Never'}"  
  26.   
  27. car.drive()  
  28. sleep 1000  
  29. println "Last accessed: ${car.lastAccessed ?: 'Never'}"  
  30.   
  31. sleep 1000  
  32. car.turnOff()  
  33.   
  34. 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. 

Supplement 
Working with closures - Declaring closures 
Working with closures - Using closures

沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!