Java.next: Расширение без наследования, часть 3

Метапрограммирование Groovy – простые решения для распространенных проблем

Языки Java.next — Groovy, Scala и Clojure — различными способами преодолевают присущие языку Java™ ограничения расширяемости. В этой статье цикла Java.next рассматриваются поразительные возможности расширения, которые дают средства метапрограммирования языка Groovy.

Нил Форд, Архитектор приложений, ThoughtWorks

Нил Форд (Neal Ford) работает архитектором приложений в ThoughtWorks, революционной компании, предоставляющей профессиональные IT-услуги и помогающей талантливым людям по всему миру эффективнее использовать программное обеспечение. Он также является проектировщиком и разработчиком приложений, учебных материалов, журнальных статей, учебных курсов, видео/DVD-презентаций и автором книг "Разработка с Delphi: Объектно-ориентированный подход", "JBuilder 3 Unleashed" и "Искусство разработки Web-приложений на Java". Он специализируется на консультациях по построению широкомасштабных корпоративных приложений. Он также является общепризнанным докладчиком и выступал на многочисленных конференциях разработчиков по всему миру. С ним можно связаться по адресу: nford@thoughtworks.com.



10.12.2013

Об этом цикле статей

Java-технология оставит в наследство не язык, а платформу. На платформе JVM работают более 200 языков программирования; какой-либо из них неминуемо вытеснит язык Java в качестве наилучшего способа программирования для JVM. Этот цикл статей посвящен исследованию трех языков нового поколения для платформы JVM — Groovy, Scala и Clojure — а также сравнению и противопоставлению новых возможностей и парадигм, чтобы дать Java-разработчикам возможность заглянуть в свое собственное ближайшее будущее.

В двух предшествующих статьях цикла Java.next я рассмотрел некоторые из множества способов, с помощью которых языки Java.next позволяют расширять существующие классы и другие артефакты. В этой статье я продолжаю это исследование и более детально рассмотрю приемы метапрограммирования Groovy, которые обеспечивают возможности расширения в самых разнообразных контекстах.

В статье Java.next: Расширение без наследования, Часть 1 я слегка коснулся некоторых функции метапрограммирования Groovy, когда рассматривал классы категорий и классExpandoMetaClass в качестве механизмов для "привинчивания" нового поведения к существующим классам. Средства метапрограммирования в Groovy этим не ограничиваются: они облегчают написание кода для интеграции с Java и помогают выполнять типовые задачи с меньшими формальностями, чем того требует язык Java.

Применение интерфейса

Интерфейсы – это распространенный семантический механизм повторного использования в языке Java. Соответственно другие языки, пытающиеся осуществить аккуратную интеграцию с Java-кодом, должны предоставлять удобные способы реализации интерфейсов. В языке Groovy классы могут расширять интерфейсы традиционным для Java способом. Кроме того, Groovy облегчает применение замыканий и отображений к экземплярам интерфейса, когда это представляется удобным.

Приведение интерфейса на основе одного метода

В примере Java-кода в листинге 1 интерфейс FilenameFilter используется для определения местоположения файлов.

Листинг 1. Отыскание файлов с помощью интерфейса FilenameFilter в Java
import java.io.File;
import java.io.FilenameFilter;

public class ListDirectories {
    public String[] listDirectoryNames(String root) {
        return new File(root).list(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return new File(name).isDirectory();
            }
        });
    }
}

В листинге 1 я создаю новый анонимный внутренний класс для определения критериев фильтрации, который заменяет метод accept(). В Groovy я могу пропустить установленную процедуру создания нового класса и просто принудительно вставить замыкание в интерфейс (см. листинг 2).

Листинг 2. Имитация интерфейса FilenameFilter в Groovy с помощью принудительного замыкания
new File('.').list(
    { File dir, String name -> new File(name).isDirectory() }
     as FilenameFilter).each { println it }

В листинге 2 метод list() ожидает поступления экземпляра FilenameFilter в качестве аргумента. Вместо этого я создаю замыкание, которое соответствует сигнатуре интерфейса accept(), и реализую функциональность интерфейса в теле замыкания. После определения замыкания я принудительно вставляю замыкание в нужный экземпляр FilenameFilter с помощью вызова as FilenameFilter. Groovy-оператор as вставляет замыкание в класс, который реализует интерфейс. Этот метод прекрасно работает для интерфейсов на основе одного метода, поскольку между методом и замыканием существует естественное соответствие.

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

Отображения

В языке Groovy для представления интерфейсов также можно использовать и отображения. Ключами отображения являются строки, которые представляют имена методов, а соответствующие значения - это блоки кода, реализующие поведение соответствующего метода. Пример в листинге 3 реализует отображение на экземпляр Iterator.

Листинг 3. Использование отображения Groovy для реализации интерфейса
h = [hasNext: { h.i > 0 }, next: {h.i--}]
h.i = 10
def iterator = h as Iterator
                                                  
while (iterator.hasNext())
  print iterator.next() + ", "
// 10, 9, 8, 7, 6, 5, 4, 3, 2, 1,

В листинге 3 я создаю отображение (h), которое содержит ключи hasNext и next, а также соответствующие им блоки кода. Groovy исходит из предположения, что ключи отображения являются строками, поэтому мне не нужно окружать эти ключи кавычками. В пределах каждого блока кода я использую точечную нотацию (h.i) для ссылки на третий ключЭта нотация, заимствованная из привычного объектного синтаксиса, представляет собой еще один пример синтаксического удобства Groovy. Блоки кода не будут выполняться, если h acts as an iterator, and I must ensure that i не ведет себя как iterator, поэтому прежде чем я буду использовать h в качестве iterator, я должен гарантировать, что i имеет соответствующее значение. Я задал значение h.i = 10. Затем я представил h в качестве Iterator и подал на его вход коллекцию целых чисел, которая начинается с числа "10".

Groovy позволяет отображениям действовать "на лету" в качестве экземпляров интерфейсов, что значительно ослабляет некоторые синтаксические трудности, порождаемые языком Java. Эта опция является прекрасной иллюстрацией того, как языки Java.next расширяют возможности разработчика.


ExpandoMetaClass

Как было показано в статье Java.next: Расширение без наследования, Часть 1, класс ExpandoMetaClass можно использовать для добавления новых методов к классам — включая такие базовые классы, как Object и String. Кроме того, класс ExpandoMetaClass полезен и при решении нескольких других задач, таких как добавление методов к экземплярам объекта и улучшение обработки исключений.

Добавление методов к объектам и удаление методов из объектов

Изменения, которые вы производите в каком-либо классе с помощью ExpandoMetaClass вступают в силу в глобальном масштабе с того момента, когда вы связываете с этим классом соответствующее поведение. Этот всеобъемлющий характер является преимуществом описываемого подхода — что неудивительно с учетом того, что этот механизм расширения является результатом разработки веб-среды Grails (см. раздел Ресурсы). Grails опирается на глобальные изменения в базовых классах. Однако иногда добавление семантики к классу требуется осуществить ограниченным образом, без распространения влияния на все экземпляры. Для таких случаев Groovy предоставляет способ взаимодействия с экземпляром метакласса объекта. Например, можно добавить методы только к определенному экземпляру объекта (см. листинг 4).

Листинг 4. Связывание поведения с экземпляром объекта
def list = new ArrayList()
list.metaClass.randomize = { ->
    Collections.shuffle(delegate)
    delegate
}

list << 1 << 2 << 3 << 4
println list.randomize() // [2, 1, 4, 3]
println list             // [2, 1, 4, 3]

В листинге 4 я создаю экземпляр (list) списка ArrayList. Затем я обращаюсь к свойству metaClass этого экземпляра, инициализируемому в отложенном (lazу) режиме. Я добавляю метод (randomize()), который возвращает коллекцию shuffle. В рамках декларации метода metaclass delegate представляет экземпляр объекта.

Однако мой метод randomize() видоизменяет базовую коллекцию, посколькуshuffle()— это видоизменяющийся вызов. Во второй строке вывода в листинге 4обратите внимание, что порядок коллекции подвергается перманентным изменениям с целью получения нового рандомизированного порядка. К счастью, мы можем с легкостью изменить поведение по умолчанию для встроенных методов, таких как Collections.shuffle(). Например, свойство randomв листинге 5 является улучшением по сравнению с методом randomize() в листинге 4.

Листинг 5. Улучшение нежелательной семантики
def list2 = new ArrayList()
list2.metaClass.getRandom = { ->
  def l = new ArrayList(delegate)
  Collections.shuffle(l)
  l
}

list2 << 1 << 2 << 3 << 4
println list2.random // [4, 1, 3, 2]
println list2        // [1, 2, 3, 4]

В листинге 5 в теле метода getRandom() список копируется перед видоизменением, поэтому исходный список остается неизменным. Кроме того, с помощью соглашения Groovy об именовании я делаю random свойством, а не методом, что обеспечивает автоматическое отображение свойств на методы get и set.

Использование механизма свойств с целью устранения лишних круглых скобок привело к недавним изменениям в связывании методов в языке Groovy. В версии 1.8 этого языка представлена концепция цепочек команд, которая позволяет более эффективно создавать специализированные предметно-ориентированные языки (DSL). Обычно DSL-языки дополняют специальным поведением существующие классы или экземпляры объектов.

Миксины

Популярным механизмом в языке Ruby и подобных ему языках являются т.н. миксины (mixin). Миксины позволяют добавлять новые методы и поля к существующим иерархиям без использования наследования. Язык Groovy также поддерживает миксины (см. листинг 6).

Листинг 6. Использование миксина для присоединения поведения
class ListUtils {
  static randomize(List list) {
    def l = new ArrayList(delegate)
    Collections.shuffle(l)
    l
  }
}
List.metaClass.mixin ListUtils

В листинге 6 я создаю хелпер-класс (ListUtils) и добавляю к нему метод randomize(). В последней строке я подмешиваю класс ListUtils в java.util.List, в результате чего мой метод randomize() становится доступен для java.util.List. Миксины можно также использовать и с экземплярами объектов. Этот механизм облегчает отладку и трассировку, поскольку ограничивает изменения отдельным артефактом кода, и потому является предпочтительным способом присоединения поведения к классам.

Комбинирование средств расширения

Возможности метапрограммирования Groovy не только эффективны по отдельности, но и могут использоваться совместно. Общее для динамических языков полезное свойство — наличие ловушки для отсутствующих методов— позволяет классу реагировать на еще не определенные методы контролируемым образом, не выдавая исключения. Если происходит вызов неизвестного метода, Groovy применяет метод methodMissing() к содержащему его классу. Метод methodMissing() можно включить в дополнения, добавленные посредством ExpandoMetaClass. Сочетание methodMissing() с ExpandoMetaClass, позволяет гибко расширять возможности существующих классов, например, Logger. Соответствующий пример показан в листинге 7.

Листинг 7. Сочетание ExpandoMetaClass и methodMissing
import java.util.logging.*

Logger.metaClass.methodMissing = { String name, args ->
    println "inside methodMissing with $name"
    int val = Level.WARNING.intValue() +
        (Level.SEVERE.intValue() - Level.WARNING.intValue()) * Math.random()
    def level = new CustomLevel(name.toUpperCase(),val)
    def impl = { Object... varArgs ->
        delegate.log(level,varArgs[0])
    }
    Logger.metaClass."$name" = impl
    impl args
}

Logger log = Logger.getLogger(this.class.name)
log.neal "really messed this up"
log.minor_mistake "can fix later"

В листинге 7 я с помощью ExpandoMetaClass присоединяю метод methodMissing() к существующему классу Logger. Теперь я могу более творчески подходить к вызову журналов в последующем коде, если этот класс Logger находится в области действия (см. три последние строки листинга 7.


Аспектно-ориентированное программирование

Аспектно-ориентированное программирование (АОП) — это популярный и полезный способ распространения технологии Java за пределы ее первоначальных конструктивных возможностей. Манипулирование байт-кодом и процессом компиляции позволяет "аспектам" внедрять новый код в существующие методы. В АОП определяется несколько терминов, в том числе pointcut— местоположение, в котором производится дополнение. Например, pointcut before относится к коду, который добавляется перед вызовом метода.

Компилятор Groovy порождает байт-код Java, поэтому АОП возможно и на языке Groovy. Однако АОП можно воспроизвести в Groovy посредством метапрограммирования, без обременительной формализации, которая требуется в Java. Класс ExpandoMetaClass позволяет вам получить доступ к методу так, чтобы можно было сохранить ссылку на этот метод. Впоследствии вы сможете переопределить этот метод, а также по-прежнему осуществлять вызов исходной версии этого метода. В листинге 8 иллюстрируется это использование класса ExpandoMetaClass для АОП:

Листинг 8. Использование класса ExpandoMetaClass для аспектно-ориентированного программирования
class Bank {
  def transfer(Account to, Account from, BigDecimal amount) {
    from.balance -= amount
    to.balance += amount
  }
}

class Account {
  def name, balance;

  @Override
  public String toString() {
    "Account{name:${name}, balance:${balance}}"
  }
}

def oldTransfer = 
  Bank.metaClass.getMetaMethod("transfer", [Account, Account, BigDecimal] as Object[])

Bank.metaClass.transfer = { Account to, Account from, BigDecimal amount ->
  println "Logging transfer: to: ${to}, from: ${from}, amount: ${amount}"
  oldTransfer.invoke(delegate, [to, from, amount] as Object[])
}

def bank = new Bank()
def acctA = new Account(name: "A", balance: 100.00)
def acctB = new Account(name: "B", balance: 200.00)
println("Balances: A = ${acctA.balance}, B = ${acctB.balance}")
bank.transfer(acctA, acctB, 10.00)
println("Balances: A = ${acctA.balance}, B = ${acctB.balance}")
//Balances: A = 100.00, B = 200.00
//Logging transfer: to: Account{name:A, balance:100.00},
//    from: Account{name:B, balance:200.00}, amount: 10.00
//Balances: A = 110.00, B = 190.00

В листинге 8 я создаю типовой класс Bank с одним методом transfer(). Вспомогательный класс Account содержит информацию о простой учетной записи. Метод ExpandoMetaClass содержит метод getMetaMethod(), который извлекает ссылку на нужный метод. В листинге 8 я применяю метод getMetaMethod() для извлечения ссылки на существующий метод transfer(). Затем с помощью ExpandoMetaClass я создаю новый метод transfer(), который замещает старый метод. В теле нового метода (после операторов журналирования) я вызываю исходный метод.

В листинге 8 содержится пример pointcut "before": Я исполняю свой "дополнительный" код до вызова исходного метода. Это распространенный подход в динамических языках, таких как Ruby, в сообществе которого этот метод получил название Monkey Patching (обезьянье исправление) (первоначально этот термин звучал как guerrilla patching ("партизанское" исправление); слово guerrilla (партизанский) созвучно слову gorilla (горилла), поэтому в результате игры слов термин из guerrilla patching сначала превратился в gorilla patching, а затем и в monkey patching). Получаемый результат аналогичен результату при использовании АОП, однако динамические расширения в Groovy позволяют реализовать это улучшение в рамках самого языка.


AST-преобразования

Несмотря на всю мощь класса ExpandoMetaClass и связанных с ним функций, они не способны охватить все точки расширения. В конечном итоге самая мощная черта метапрограммирования — это его способность модифицировать AST-дерево компилятора (Abstract Syntax Tree) — внутреннюю структуру данных, которая поддерживается процессом компиляции. Аннотации — это одно из местоположений, позволяющих подключить преобразования. В языке Groovy предопределено несколько полезных расширений, таких как AST-преобразования.

В частности, аннотация@Lazy, например, (@Lazy pets = ['Cat', 'Dog', 'Bird']) задерживает инстанцирование структур данных до того момента, пока они не потребуются для вычисления. В версии Groovy 1.8 представлено множество полезных структурных аннотаций, несколько из которых присутствует в листинге 9:

Листинг 9. Полезные структурные аннотации в Groovy
import groovy.transform.*;

@Immutable
@TupleConstructor
@EqualsAndHashCode
@ToString(includeNames = true, includeFields=true)
final class Point {
  int x
  int y
}

В листинге 9 среда исполнения Groovy автоматически выполняет следующие действия.

  • Генерирует конструкторы в стиле tuple
  • Генерирует метод equals() и метод hashCode() methods
  • Делает класс Point неизменным
  • Генерирует метод toString()

Применение AST-преобразований существенно эффективнее применения IDE или отражения (reflection) для генерации инфраструктурных методов. В случае IDE необходимо постоянно помнить про повторную генерацию методов в случае возникновения изменений. В свою очередь, отражение работает медленнее, чем генерация кода, производимая на этапе компиляции.

В дополнение к применению обширного ассортимента готовых AST-преобразований вы можете использовать полный API-интерфейс, который Groovy предоставляет разработчику для создания собственных преобразований. Этот API позволяет вносить изменения в генерацию кода, используя самую детальную из нижележащих абстракций.


Заключение

В статье описано множество вариантов расширения, которые язык Groovy предоставляет благодаря своим возможностям метапрограммирования. В следующей статье цикла Java.next я рассмотрю различные средства метапрограммирования языка Scala.

Ресурсы

Научиться

  • Оригинал статьи: Java.next: Extension without inheritance, Part 3.
  • The Productive Programmer (Продуктивный программист) (Neal Ford, O'Reilly Media, 2008): В своей книге Нил Форд подробно останавливается на многих темах, рассматриваемых в данном цикле.
  • Clojure: современный функциональный вариант Lisp, работающий на платформе JVM.
  • Scala: современный функциональный язык на платформе JVM.
  • Groovy: динамическая разновидность Java с обновленным синтаксисом и расширенными возможностями.
  • Grails: популярная веб-среда, написанная на языке Groovy.
  • Practically Groovy (Andrew Glover и Scott Davis, developerWorks, 2004-2009): Цикл статей на developerWorks, посвященных практическим аспектам успешного применения Groovy.
  • Mastering Grails (Scott Davis, developerWorks, 2008-2009): Цикл статей, посвященных Grails.
  • Advanced Groovy Tips and Tricks: из этой превосходной презентации Кена Коусена (Ken Kousen) позаимствован ряд продвинутых примеров использования Groovy, приведенных в данной статье.
  • Функциональное мышление: : колонка Нила Форда на developerWorks, посвященная функциональному программированию.
  • Другие статьи этого автора (Нил Форд, developerWorks, с июня 2005 г. по настоящее время): о Groovy, Scala, Clojure, функциональном программировании, архитектуре, дизайне, Ruby, Eclipse и других Java-технологиях.
  • Раздел developerWorks, посвященный Java-технологии: сотни статей по всем аспектам программирования на Java.

Получить продукты и технологии

Обсудить

  • Присоединяйтесь к сообществу developerWorks . Связывайтесь с другими пользователями developerWorks и знакомьтесь с ориентированными на разработчиков форумами, , блогами, группами и вики-ресурсами.

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java, Open source
ArticleID=956706
ArticleTitle=Java.next: Расширение без наследования, часть 3
publish-date=12102013