Статьи

Ввод Proxy-o-Matic для работы

Все началось пару недель назад, когда Алекс Ткачман представил следующий код в список разработчиков Groovy.

as(MouseListener) {
mouseClicked{
println "Clicked: $it"
}

mousePressed {
println "Pressed: $it"
}
...
}

Этот маленький кусочек кода должен создать экземпляр чего-то (неизвестного класса), который реализует интерфейс MouseListener, другими словами, он создает прокси MouseListener. Но прежде чем мы перейдем к теме, давайте вернемся к тому, что Groovy уже предлагает: возможность создать прокси из некоторой предопределенной структуры, в данном случае Map или Closure, с учетом следующих правил.

  • Вы должны использовать как ключевое слово
  • при принудительном закрытии, целевой интерфейс должен определять только один метод
  • при навязывании карты целью может быть интерфейс, абстрактный или конкретный класс

Эти правила позволяют вам легко создавать прокси, и хотя карты позволяют вам использовать прокси 3 различных цели, они имеют два недостатка.

  • карты должны содержать уникальный ключ для каждого имени метода, вы не можете прокси перегружать определения методов
  • вы не можете вызвать прокси-метод внутри другого прокси-метода

 К счастью, в Groovy есть очень удобная альтернатива динамическим bean-компонентам: Expandos. Если вы еще не видели раскрытия, подумайте о них как об объектах JSON. Да, я имел в виду именно это. Вы можете добавить свойства по желанию в Expando, если значение свойства оказывается замыканием, тогда это свойство будет обрабатываться как определение метода, давайте рассмотрим следующий пример

def bean = new Expando()
bean.foo = {-> "foo" }
bean.bar = {-> "bar" }
bean.foobar = {-> foo()+bar() }
assert bean.foo() == "foo"
assert bean.bar() == "bar"
assert bean.foobar() == "foobar"

Хорошо, экспансии могут преодолеть ограничение № 2 на картах, но, поскольку они также основаны на ограничениях № 1, все еще применяется, и, к сожалению, в эксплофосе нет прокси-дружественного механизма, связанного с ключевым словом as (который на самом деле работает, вызывая asType (Class) на цель).

Введите Proxy-o-Matic .

Основной класс Proxy-o-Matic (метко названный ProxyOMatic) предоставляет очень простой API для гомогенизокритирования прокси из трех источников, которые мы обрисовали в общих чертах: замыкания, карты и расширения, и предоставляет функции, которые преодолевают некоторые из ограничений, которые вышеупомянутые правила не могут ломать. Прокси, созданные с помощью ProxyOMatic.proxy (), имеют следующие функции

  • вызывать прокси-методы из других прокси-методов
  • может прокси более одного интерфейса одновременно

Прокси, созданные из замыканий, могут

  • определить реализации методов с синтаксисом, аналогичным тому, как определяются классы
  • определить реализации методов для перегруженных методов

Прокси, созданные из карт, могут

  •  определить реализации методов для перегруженных методов, при условии, что они используют ProxyMethodKey в качестве ключей вместо строк

До сих пор Expandos не получает ничего, кроме общих функций. Но достаточно теории, код должен показать вам лучшую картину, давайте возьмем следующие определения интерфейса, например

interface Foo { String foo() }
interface Bar { String bar() }
interface FooBar extends Foo, Bar {
String foobar()
}
interface Fooz extends Foo {
String foo( String n )
}

Создание прокси из замыканий и Карт для первых трех так же просто, как

import static org.kordamp.groovy.util.ProxyOMatic.proxy

def fc = proxy( Foo ) {
foo { -> "Foo" }
}
def fbc = proxy( FooBar ) {
foo { -> "Foo" }
bar { -> "Bar" }
foobar { -> foo() + bar() }
}

def fm = proxy( Foo, [
foo: { -> "Foo" }
])
def fbm = proxy( FooBar, [
foo: { -> "Foo" },
bar: { -> "Bar" },
foobar: { -> foo() + bar() }
])

// assert proxies are of the required type
assert fc instanceof Foo
assert fm instanceof Foo
assert fbc instanceof FooBar
assert fbm instanceof Foobar

// assert methods return expected results
assert [fc.foo(),fm.foo()] == ["Foo","Foo"]
assert [fbc.bar(),fbm.bar()] == ["Bar","Bar"]
assert [fbc.foobar(),fbm.foobar()] == ["FooBar","FooBar"]

Вы, наверное, интересуетесь перегруженной версией метода, без лишних слов

// don't forget to add the following line
// import static org.kordamp.groovy.util.ProxyOMatic.methodKey

def fzc = proxy( Fooz ) {
foo { -> "Foo" }
foo { String n -> "Foo" + n }
}
def map = [:]
map[methodKey("foo")] = { -> "Foo" }
map[methodKey("foo",[String])] = { String n -> "Foo" + n }
def fzm = proxy( Fooz, map )

assert fzc.foo() == "Foo"
assert fzc.foo("Groovy") == "FooGroovy"
assert fzm.foo() == "Foo"
assert fzm.foo("Groovy") == "FooGroovy"

Предупреждаю , methodKey доступен только в текущей версии разработки (0.6-SNAPSHOT), и пока мы здесь, позвольте мне показать вам, что готовится к выпуску в скором времени. Вы, наверное, уже поняли, что эти прокси-серверы для своей работы полагаются на java.lang.reflect.Proxy, поэтому они связаны законами, управляющими Proxy, но эти парни хотят быть Groovy, поэтому они автоматически реализуют интерфейс GroovyObject , что дает им доступ к следующим Groovylicious методов: 

  • GetProperty
  • SetProperty
  • methodMissing
  • propertyMissing (режим получения)
  • propertyMissing (установить режим)
  • getMetaClass
  • setMetaClass (только для чтения, не может его изменить)

По соглашению, если реализация метода не была предоставлена, прокси вызовет methodMissing , который по умолчанию генерирует исключение UnsupportedOperationException, это означает, что следующий код проходит зеленый тест

import static org.kordamp.groovy.util.ProxyOMatic.proxy

def shouldFail = { Class ex, code ->
try {
code()
throw new RuntimeException()
}catch( Exception x ) {
assert x.class == ex
}
}

interface Foo { String foo() }

def f1 = proxy( Foo ) { }
shouldFail( UnsupportedOperationException ) {
f1.foo()
}

Что произойдет, если вы вызовете метод, для которого не было определено ни одного из прокси-интерфейсов?

import static org.kordamp.groovy.util.ProxyOMatic.proxy

interface Foo { String foo() }

def f2 = proxy( Foo ) {
foo { -> bar() }
methodMissing { String name, value -> "oops" }
}
assert f2.foo() == "oops"
assert f2.bar() == "oops"

Свойства работают очень похоже, например, вот как вы можете представить локальную переменную как свойство на прокси

import static org.kordamp.groovy.util.ProxyOMatic.proxy

interface Foo { String foo() }

def f3 = proxy( Foo ) {
def count = 0
foo { -> count++ }
propertyMissing { String name -> count }
}
assert f3.foo() == 0
assert f3.foo() == 1
assert f3.count == 2

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

import static org.kordamp.groovy.util.ProxyOMatic.proxy

interface Foo { String foo() }

def f4 = proxy( Foo ) {
foo { -> "${id}:${name}".toString() }

properties {
name()
id(1)
}
}
f4.name = "Duke"
assert f4.foo() == "1:Duke"

Узел свойств работает для 3 источников (Closures, Maps и Expandos). Если вы определяете значение для определения свойства, то это значение будет назначено в качестве начального значения свойства, как вы можете проверить, посмотрев, как было определено свойство id. Вы можете перехватить доступ к свойству, определив setProperty / getProperty, но есть ловушка. Вы не можете вызывать их напрямую (по крайней мере, не во время тестирования внутри скрипта), потому что включающий скрипт определяет свой собственный getProperty / setProperty, поэтому всякий раз, когда вызывается замыкание, которое вызывает любой из этих методов, оно будет вызывать методы скрипта, не те, которые вы определили. У обычных классов такой проблемы нет, и если она неоднозначна, вы можете квалифицировать вызов с помощью этого. Очевидно, что выиграл »не работает (используя это) и что теперь? прокси предоставляют доступное только для чтения свойство с именем self , которое в значительной степени работает следующим образом .

import static org.kordamp.groovy.util.ProxyOMatic.proxy

interface Foo { String foo( String n ) }

def f5 = proxy( Foo ) {
def props = [:]
foo { String n -> self.setProperty(n,n); n }
getProperty { String name -> props[name] }
setProperty { String name, value -> props[name] = value }
}
assert f5.name == null
assert f5.foo("name") == "name"
assert f5.name == "name"

Вы, наверное, уже думаете: «прокси — это здорово, но зачем перебирать все эти хлопоты?» ну вот несколько причин 

  • Вы можете передать Expando или даже Map классу Groovy, который поддерживает типизацию утки, к сожалению, вы не можете сделать то же самое для класса Java, который ожидает определенный тип
  • Текущие механизмы создания прокси в Groovy не поддерживают перегруженные методы
  • Groovy не поддерживает определения внутренних классов (пока анонимные внутренние классы запланированы для 1.7)

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

Возвращаясь к предложениям Алекса, Прокси-о-Матик очень близок. Теперь, чтобы понять, как создавать прокси из абстрактных и конкретных классов.