Статьи

Groovy замыкания не имеют доступа к закрытым методам в суперклассе


Недавно я натолкнулся на отличную странность. (По крайней мере, это воспринимается мной как странность). Замыкания в классном классе не имеют доступа к закрытому методу, если этот метод определен в суперклассе. Это кажется странным в сочетании с тем фактом, что обычные методы в суперклассе могут получить доступ к закрытому методу, определенному в суперклассе.

Фоновые

замыкания Groovy имеют такой же доступ к области действия для переменных и методов членов класса, что и обычный метод groovy. Другими словами, замыкания связаны с переменными в той области, в которой они определены. Смотрите ссылку на Codehaus для официальной документации:

http://groovy.codehaus.org/Closures

Это подразумевает, что замыкание будет воспроизводиться по правилам объектной ориентации на языке Java. Однако я обнаружил, что у замыканий нет доступа к закрытым методам, которые определены в суперклассе.

Лучший способ продемонстрировать это на коротком примере из Grails. Я использовал TDD для примера. Обратите внимание, что это фиктивный случай без каких-либо деловых целей. Позже я предложу более разумный сценарий в бизнес-контексте, где я столкнулся с этим сценарием.

Простой пример Groovy

Возьмем класс с закрытием, открытый метод и закрытый метод. Затем расширить этот класс. Попробуйте вызвать закрытие. Мы получаем ошибку.

package closure.access

class SendCheckService {

  def calendarService

  /**
   * Closure that invokes a private method
   */
  def closureToSendCheck = {
    sendPersonalCheck()
  }

  def regularMethod() {
    sendPersonalCheck()
  }

  /*
   * Private method that we want to see executed
   */
  private sendPersonalCheck() {
    println "Sending Personal Check"
  }
}

class FooService extends SendCheckService {

}

Вот несколько тестов, демонстрирующих ошибку.

package closure.access

import grails.test.*

class SendCheckServiceTests extends GrailsUnitTestCase {

  def sendCheckService

  protected void setUp() {
    super.setUp()
    sendCheckService = new SendCheckService()
  }

  /**
   * Invocation of the closure from the super class prints out the message:
   * "Sending Personal Check"
   */
  void testClosureToSendCheck() {
    sendCheckService.closureToSendCheck()
  }

  /**
   * Invocation of the method from the super class prints out the message:
   * "Sending Personal Check"
   */
  void testRegularMethod() {
    sendCheckService.regularMethod()
  }

  /**
   * Invocation of the the closure from the subclass yields an error:
   * groovy.lang.MissingMethodException: No signature of method: closure.access.FooService.sendPersonalCheck()
   *   is applicable for argument types: () values: []
   * This essentially means it does not exists.
   */
  void testFoo_ClosureToSendCheck() {
    def fooService = new FooService()
    fooService.closureToSendCheck()
  }

  /**
   * Invocation of the method does not yield and error!
   */
  void testFoo_RegularMethod() {
    def fooService = new FooService()
    fooService.regularMethod()
  }
} 

Все хорошо

  • testClosureToSendCheck () выполняется нормально, когда замыкание может получить доступ к закрытому методу. Это как и ожидалось. Это все хорошо, потому что мы еще не продлили класс.
  • testRegularMethod () просто демонстрирует обычные принципы ОО. Метод может вызывать частные методы.

Не так, как ожидалось

  • testFoo_ClosureToSendCheck () вызывает подкласс SendCheckService. Он вызывает то же самое закрытие, но мы получаем исключение MissingMethodException.
  • testFoo_RegularMethod () просто контрастирует с testFoo_ClosureToSendCheck (). Я вызвал этот тест, чтобы показать, что у нас должна быть возможность закрытия доступа к закрытым методам, потому что другие обычные методы могут!

Зачем даже пытаться


вызвать замыкание Закрытый метод Это может быть
двойной учебный вопрос, который может задать любой умный разработчик. Это вопросы, почему мы должны даже войти в этот беспорядок. Это верный момент и должен быть объяснен.

Оптимально использовать замыкания в попытке повторно использовать существующую логику шаблона. Позвольте мне привести простую бизнес-проблему.

Представьте, что нам нужно кодировать систему, которая отправляет различные типы чеков: личные чеки и бизнес-чеки. У нас есть условие, что эти два события НИКОГДА не должны проводиться вместе. Отправляйте персональный чек только в одном случае, а бизнес-чек — в другое время. Тем не менее, они оба должны следовать той же логике. Они должны быть отправлены в рабочий день (без праздников или выходных). Таким образом, у нас есть сценарий, в котором им нужна одинаковая календарная логика, но она нужна отдельно.
Дублирующая логика Мы могли бы просто кодировать логику календаря дважды. (Помните, что отправка бизнес-чеков и личных чеков вместе в одном запросе невозможна!)



package closure.access

class SendCheckService2 {

  def calendarService

  def triggerPersonalCheck () {
    if (calendarService.todayIsBusinessDay()) {
      sendPersonalCheck()  
    } else {
      println "DO NOTHING"
    }
  }

  def triggerBusinessCheck() {
    if (calendarService.todayIsBusinessDay()) {
      sendBusinessCheck()
    } else {
      println "DO NOTHING"
    }
  }

  private sendPersonalCheck() {
    println "Sending Personal Check"
  }

  private sendBusinessCheck() {
    println "Sending Business Check"
  }  
}

Используйте принцип СУХОГО

Чтобы избежать этого и следовать СУХОЙ, мы можем использовать замыкания. Создайте метод, который принимает замыкание, и передайте его фрагментам кода для выполнения в замыкании. В результате у нас есть календарная логика, определенная один раз, но выполненная отдельно для другого фрагмента кода.

package closure.access

class SendCheckService3 {

  def calendarService

  def triggerPersonalCheck() {
    checkIfBusinessDayAndExecute(sendBusinessCheck)
  }

  def triggerBusinessCheck() {
    checkIfBusinessDayAndExecute(sendBusinessCheck)
  }

  def checkIfBusinessDayAndExecute(Closure closure) {
    if (calendarService.todayIsBusinessDay()) {
      closure()
    } else {
      println "DO NOTHING"
    }
  }

  /**
   * This is now a closure we can pass around
   */
  def sendPersonalCheck = {
    println "Sending Personal Check"
  }

  /**
   * This is now a closure we can pass around
   */  
  def sendBusinessCheck = {
    println "Sending Business Check"
  }  
}

В моем сценарии реального мира, где я столкнулся с проблемой закрытия, случилось, что мое закрытие пыталось выполнить закрытый метод в абстрактном классе. Вот где я заметил проблему.
Мысли JVM Я, честно говоря, не знаю кровавых подробностей, почему замыкания в суперклассах не могут получить доступ к закрытым методам, но у меня есть идея. Groovy создает замыкания, компилируя их как внутренние классы. Поскольку подкласс расширяет суперкласс, а затем содержит замыкание, внутренний класс не имеет доступа к методам суперкласса. Причина, по которой метод имеет доступ, заключается в том, что он скомпилирован как один экземпляр класса. Реализация замыкания не такая (будучи внутренним классом), и поэтому мы видим нарушение объектно-ориентированного поведения.



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

Спасибо Скотту Риску за помощь с примерами.