Статьи

Метапрограммирование с Groovy II: ExpandoMetaClass

Во второй части этой серии вы узнаете о ExpandoMetaClass, специальном MetaClass, который позволит вам обновлять и настраивать поведение классов во время выполнения.

Вступление

В первой части мы увидели, что Groovy предоставляет MetaClass для каждого класса, и что вы можете изменять поведение классов с помощью категорий, которые, как легко выглядят, обычно связаны с временной областью. ExpandoMetaClass, с другой стороны, может обеспечить более постоянное обновленное поведение (пока ваше приложение / скрипт все еще работает).

Скимминг поверхности 

Давайте вернемся к примеру с commons-lang, добавив методы capitalize () и normalize () к String с использованием категории, на этот раз мы не будем использовать StringUtils как категорию, а создадим собственную StringCategory.

import org.apache.commons.lang.StringUtils  

class StringCategory {
static capitalize( String self ){
StringUtils.capitalize(self)
}

static normalize( String self ){
self.split("_").collect { word ->
word.toLowerCase().capitalize()
}.join("")
}
}

use( StringCategory ){
assert "Groovy" == "groovy".capitalize()
assert "CamelCase" == "CAMEL_CASE".normalize()

Помните, что если вы попытаетесь вызвать любой из этих методов за пределами use (), будет выдано исключение. Теперь для версии ExpandoMetaClass

import org.apache.commons.lang.StringUtils  

String.metaClass.capitalize = {
StringUtils.capitalize(delegate)
}

String.metaClass.normalize = {
delegate.split("_").collect { word ->
word.toLowerCase().capitalize()
}.join("")
}

assert "Groovy" == "groovy".capitalize()
assert "CamelCase" == "CAMEL_CASE".normalize()

Обратите внимание на незначительные изменения, для методов с заглавными буквами и нормализации не требуется никаких параметров, потому что вместо self мы используем делегат замыкания в качестве целевого объекта, на который будет действовать метод, за исключением того, что код поведения идентичен. Основное отличие состоит в том, как эти методы регистрируются в системе MOP. В случае ExpandoMetaClass вы можете добавить новый метод или обновить / перегрузить существующий, назначив замыкание в качестве свойства MetaClass. Давайте рассмотрим другой пример, начиная с простого (очень простого) POGO, имеющего только одно свойство.

class Person {
String name
}

Person.metaClass.greet = {
"Hello, I'm $name"
}
def duke = new Person(name:'Duke')
assert duke.greet() == "Hello, I'm Duke"

Ничего особенного, но полезного, теперь давайте попробуем добавить новый метод и перегрузить его одновременно

Person.metaClass.whatIsThis = { String arg ->
"it is a String with value '$arg'"
}
Person.metaClass.whatIsThis << { int arg ->
"it is an int with value $arg"
}

assert duke.whatIsThis( 'Groovy' ) == "it is a String with value 'Groovy'"
assert duke.whatIsThis( 12345 ) == "it is an int with value 12345"

Сначала мы определяем метод whatIsThis (), который принимает String в качестве параметра (обратите внимание, что мы используем =, чтобы назначить замыкание для свойства метакласса), затем добавляем перегруженную версию, которая принимает int в качестве параметра, обратите внимание, что мы используем оператор leftShit чтобы добавить новый метод, если бы мы использовали оператор присваивания, мы бы перезаписали и не перегружали whatIsThis () . Что если мы решили перегрузить метод во второй раз, но не указали тип? что случилось бы?

Person.metaClass.whatIsThis << { arg ->
"I don't know what $arg is"
}

assert duke.whatIsThis( 1.0 ) == "I don't know what 1.0 is"
assert duke.whatIsThis( 'Groovy' ) == "it is a String with value 'Groovy'"
assert duke.whatIsThis( 12345 ) == "it is an int with value 12345"

Благодаря динамической диспетчеризации Groovy найден и вызван правильный перегруженный метод. 

Погружение глубже

Система MOP Groovy включает в себя некоторые точки расширения / перехвата, которые можно использовать для изменения поведения класса или объекта, главным образом

  • getProperty / setProperty: управление доступом к свойству
  • invokeMethod: управляет вызовом метода, вы можете использовать его для настройки параметров существующих методов или перехвата еще не существующих
  • methodMissing: предпочтительный способ перехвата несуществующих методов
  • propertyMissing: также предпочтительный способ перехватить несуществующие свойства

Давайте перехватим все вызовы методов , используя invokeMethod , даже для тех методов, которые на самом деле не были определены в нашем тестовом классе

class Person {
String name
String sayHello( toWhom ){ "Hello $toWhom" }
}

Person.metaClass.invokeMethod = { String name, args ->
"$name() called with $args"
}

def duke = new Person(name:'Duke')
assert duke.sayHello('world') == 'sayHello() called with {"world"}'
assert duke.greet() == 'greet() called with {}'
assert duke.greet('world') == 'greet() called with {"world"}'

Теперь давайте попробуем то же самое, но на этот раз, используя methodMissing

class Person {
String name
String sayHello( toWhom ){ "Hello $toWhom" }
}

Person.metaClass.methodMissing = { String name, args ->
"$name() called with $args"
}

def duke = new Person(name:'Duke')
assert duke.sayHello('world') == 'Hello world'
assert duke.greet() == 'greet() called with {}'
assert duke.greet('world') == 'greet() called with {"world"}'

На этот раз ранее существовавший метод sayHello () не был перехвачен, потому что он действительно был определен в тестовом классе, но методы greet () не были перехвачены. Добавление новых свойств может оказаться сложнее, чем вы думаете, особенно когда речь идет о сохранении значения, установленного для этих новых свойств. Из — за детали реализации может возникнуть необходимость использовать более постоянное хранение , как синхронизированная карта, поскольку свойства могут быть добавлены к Метаклассу , но могут быть мусор без вас зная об этом (см здесь для объяснения). Существуют и другие вещи, которые ExpandoMetaClass позволит вам сделать, например, перегрузка статических методов. Пожалуйста, ознакомьтесь с документацией ExpandoMetaClass, чтобы узнать больше об этих функциях.

Поймать

ExpandoMetaClass — это значительное улучшение по сравнению с тем, что Groovy ранее предоставлял в отделе пользовательских метаклассов, но он также имеет свои причуды, например, наследование не включено по умолчанию (потому что это приведет к снижению производительности), следующий код завершится ошибкой, несмотря на его простоту

class Person {
String name
}
class Employee extends Person {
int employeeId
}

Person.metaClass.greet = { -> "Hello!" }

def duke = new Person(name:'Duke')
assert duke.greet() == "Hello!"
def worker = new Employee(name:'Drone1')
// the following line causes an exception!!
assert worker.greet() == "Hello!"

Обходной путь активируется путем вызова ExpandoMetaClass.enableGlobally (), но используйте его разумно.

Вывод

ExpandoMetaClass имеет более длительный срок службы, чем категория, он также может влиять на подклассы, поскольку категории, хотя и со штрафом. Тем не менее, это отличное дополнение к языку, изначально созданный для проекта Grails .