Когда вы исправляете метод экземпляра, можете ли вы вызвать переопределенный метод из новой реализации?

445

Скажем, я обезьяна, исправляющая метод в классе, как я могу вызвать переопределенный метод из переопределяющего метода? Т.е. что-то вродеsuper

Например

class Foo
  def bar()
    "Hello"
  end
end 

class Foo
  def bar()
    super() + " World"
  end
end

>> Foo.new.bar == "Hello World"
Джеймс Холлингворт
источник
Разве первый класс Foo не должен быть каким-то другим, а второй класс Foo наследуется от него?
Драко Атер
1
Нет, я исправляю обезьяну. Я надеялся, что будет что-то вроде super (), которое я мог бы использовать для вызова оригинального метода
Джеймс Холлингворт
1
Это необходимо, когда вы не контролируете создание Foo и использование Foo::bar. Таким образом, вы должны обезьяна патч метод.
Халил Озгюр

Ответы:

1167

РЕДАКТИРОВАТЬ : Прошло 9 лет с тех пор, как я первоначально написал этот ответ, и это заслуживает некоторой косметической операции, чтобы сохранить его в актуальном состоянии.

Вы можете увидеть последнюю версию перед редактированием здесь .


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

Избежание мартышек

наследование

Итак, если это вообще возможно, вы должны предпочесть что-то вроде этого:

class Foo
  def bar
    'Hello'
  end
end 

class ExtendedFoo < Foo
  def bar
    super + ' World'
  end
end

ExtendedFoo.new.bar # => 'Hello World'

Это работает, если вы контролируете создание Fooобъектов. Просто измените каждое место, которое создает Fooвместо того, чтобы создать ExtendedFoo. Это работает даже лучше , если вы используете Dependency Injection Design Pattern , то шаблон Factory Method Design , то шаблон Abstract Factory Design или что - то вдоль этих линий, так как в этом случае, есть только место вам нужно изменить.

Делегация

Если вы не контролируете создание Fooобъектов, например, потому что они создаются структурой, которая находится вне вашего контроля (например,например), тогда вы можете использовать шаблон дизайна Wrapper :

require 'delegate'

class Foo
  def bar
    'Hello'
  end
end 

class WrappedFoo < DelegateClass(Foo)
  def initialize(wrapped_foo)
    super
  end

  def bar
    super + ' World'
  end
end

foo = Foo.new # this is not actually in your code, it comes from somewhere else

wrapped_foo = WrappedFoo.new(foo) # this is under your control

wrapped_foo.bar # => 'Hello World'

По сути, на границе системы, где Fooобъект входит в ваш код, вы заключаете его в другой объект, а затем используете этот объект вместо исходного везде в вашем коде.

Это использует Object#DelegateClass вспомогательный метод из delegateбиблиотеки в stdlib.

«Чистая» мартышка

Module#prepend: Миксин

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

Module#prependбыл добавлен для поддержки более или менее точно этот вариант использования. Module#prependделает то же самое Module#include, за исключением того, что он смешивается в миксине непосредственно под классом:

class Foo
  def bar
    'Hello'
  end
end 

module FooExtensions
  def bar
    super + ' World'
  end
end

class Foo
  prepend FooExtensions
end

Foo.new.bar # => 'Hello World'

Примечание: я также написал немного о Module#prepend этом вопросе: модуль Ruby предваряет против деривации

Mixin Inheritance (не работает)

Я видел, как некоторые люди пытаются (и спрашивают о том, почему это не работает здесь, в StackOverflow) что-то вроде этого, то есть, includeиспользуя миксин вместо prependэтого:

class Foo
  def bar
    'Hello'
  end
end 

module FooExtensions
  def bar
    super + ' World'
  end
end

class Foo
  include FooExtensions
end

К сожалению, это не сработает. Это хорошая идея, потому что она использует наследование, что означает, что вы можете использовать super. Тем не менее, Module#includeвставляет mixin над классом в иерархию наследования, что означает, что FooExtensions#barон никогда не будет вызван (и если бы он был вызван, то superон фактически не ссылался бы на него, Foo#barа скорее на Object#barкоторый не существует), поскольку Foo#barвсегда будет найден первым.

Обертывание методом

Большой вопрос: как мы можем держаться за barметод, фактически не оставляя фактического метода ? Ответ, как это часто бывает, заключается в функциональном программировании. Мы получаем метод как фактический объект и используем замыкание (то есть блок), чтобы убедиться, что мы и только мы удерживаем этот объект:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  old_bar = instance_method(:bar)

  define_method(:bar) do
    old_bar.bind(self).() + ' World'
  end
end

Foo.new.bar # => 'Hello World'

Это очень чисто: так как old_barэто просто локальная переменная, она выйдет из области видимости в конце тела класса, и к ней невозможно получить доступ откуда угодно, даже используя отражение! И поскольку он Module#define_methodзанимает блок, а блоки закрываются вокруг окружающей его лексической среды (именно поэтому мы используем define_methodего defздесь), то онтолько он) будет по-прежнему иметь доступ old_barдаже после того, как он выйдет из области видимости.

Краткое объяснение:

old_bar = instance_method(:bar)

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

old_bar.bind(self)

Это немного сложно. По сути, в Ruby (и почти во всех языках ОО на основе одной диспетчеризации) метод привязан к конкретному объекту-получателю, называемому selfв Ruby. Другими словами: метод всегда знает, к какому объекту он был вызван, он знает, к какомуself такое. Но мы взяли метод непосредственно из класса, откуда он знает, что это selfтакое?

Ну, это не так, поэтому мы должны bindOUR UnboundMethodк объекту первого, который будет возвращать Methodобъект , который мы можем назвать. ( UnboundMethodне могут быть вызваны, потому что они не знают, что делать, не зная ихself .)

И к чему мы bindэто? Мы просто bindэто для себя, так он будет вести себя так же, как оригинал bar!

Наконец, нам нужно позвонить, Methodчто возвращается из bind. В Ruby 1.9 есть некоторый новый синтаксис для этого ( .()), но если вы используете 1.8, вы можете просто использоватьcall метод; это то, что .()переводится в любом случае.

Вот пара других вопросов, где объясняются некоторые из этих концепций:

«Грязная» мартышка

alias_method цепь

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

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  alias_method :old_bar, :bar

  def bar
    old_bar + ' World'
  end
end

Foo.new.bar # => 'Hello World'
Foo.new.old_bar # => 'Hello'

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

Несмотря на то, что это имеет некоторые нежелательные свойства, оно, к сожалению, стало популярным благодаря AciveSupport's Module#alias_method_chain.

В сторону: уточнения

Если вам нужно другое поведение только в нескольких определенных местах, а не во всей системе, вы можете использовать уточнения, чтобы ограничить патч обезьяны определенной областью. Я собираюсь продемонстрировать это здесь на Module#prependпримере сверху:

class Foo
  def bar
    'Hello'
  end
end 

module ExtendedFoo
  module FooExtensions
    def bar
      super + ' World'
    end
  end

  refine Foo do
    prepend FooExtensions
  end
end

Foo.new.bar # => 'Hello'
# We haven’t activated our Refinement yet!

using ExtendedFoo
# Activate our Refinement

Foo.new.bar # => 'Hello World'
# There it is!

Вы можете увидеть более сложный пример использования уточнений в этом вопросе: Как включить патч обезьяны для конкретного метода?


Заброшенные идеи

До того, как сообщество Ruby приняло решение Module#prepend, вокруг было много разных идей, на которые вы иногда можете встретить ссылки в более ранних обсуждениях. Все это относится к категории Module#prepend.

Комбинаторы методов

Одной из идей была идея комбинаторов методов из CLOS. Это в основном очень облегченная версия подмножества Аспектно-ориентированного программирования.

Используя синтаксис как

class Foo
  def bar:before
    # will always run before bar, when bar is called
  end

  def bar:after
    # will always run after bar, when bar is called
    # may or may not be able to access and/or change bar’s return value
  end
end

Вы сможете «зацепиться» за исполнение bar метода.

Однако не совсем понятно, если и как вы получаете доступ к barвозвращаемому значению в пределах bar:after. Может быть, мы могли бы (ab) использовать superключевое слово?

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  def bar:after
    super + ' World'
  end
end

замена

Комбинатор before эквивалентен prependсмешиванию с переопределенным методом, который вызывает superв самом конце метода. Аналогично, после комбинатор эквивалентен prependсмешиванию с переопределенным методом, который вызывает superв самом начале метода.

Вы также можете делать вещи до и после вызова super, вы можете вызывать superнесколько раз, а также получать и манипулировать superвозвращаемым значением, делая его prependболее мощным, чем комбинаторы методов.

class Foo
  def bar:before
    # will always run before bar, when bar is called
  end
end

# is the same as

module BarBefore
  def bar
    # will always run before bar, when bar is called
    super
  end
end

class Foo
  prepend BarBefore
end

а также

class Foo
  def bar:after
    # will always run after bar, when bar is called
    # may or may not be able to access and/or change bar’s return value
  end
end

# is the same as

class BarAfter
  def bar
    original_return_value = super
    # will always run after bar, when bar is called
    # has access to and can change bar’s return value
  end
end

class Foo
  prepend BarAfter
end

old ключевое слово

Эта идея добавляет новое ключевое слово, похожее на super, которое позволяет вам вызывать перезаписанный метод таким же образом, superпозволяет вызывать переопределенный метод:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  def bar
    old + ' World'
  end
end

Foo.new.bar # => 'Hello World'

Основная проблема в том, что он несовместим в обратном направлении: если у вас есть вызванный метод old, вы больше не сможете его вызывать!

замена

superв методе переопределения в prepended mixin, по существу, такой же, как oldв этом предложении.

redef ключевое слово

Как и выше, но вместо добавления нового ключевого слова для вызова перезаписанного метода и оставления в defпокое, мы добавляем новое ключевое слово для переопределения методов. Это обратно совместимо, так как синтаксис в настоящее время в любом случае недопустим:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    old + ' World'
  end
end

Foo.new.bar # => 'Hello World'

Вместо добавления двух новых ключевых слов, мы также можем переопределить значение superinside redef:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    super + ' World'
  end
end

Foo.new.bar # => 'Hello World'

замена

redefИспользование метода эквивалентно переопределению метода в prepended mixin. superв методе переопределения ведет себя как superили oldв этом предложении.

Йорг Миттаг
источник
@ Jörg W Mittag, безопасен ли подход с переносом методов? Что происходит, когда два параллельных потока вызывают bindодну и ту же old_methodпеременную?
Хариш Шетти
1
@KandadaBoggu: Я пытаюсь понять, что именно вы подразумеваете под этим :-) Однако я уверен, что он не менее поточно-ориентирован, чем любой другой вид метапрограммирования в Ruby. В частности, каждый вызов UnboundMethod#bindвозвращает новый, другой Method, поэтому я не вижу никакого конфликта, независимо от того, вызываете ли вы его дважды подряд или два раза одновременно из разных потоков.
Йорг Миттаг
1
С тех пор, как я начал работать с рубином и рельсами, искал объяснения по поводу такого исправления. Отличный ответ! Единственное, чего мне не хватало, это заметки о class_eval и повторном открытии класса. Вот оно: stackoverflow.com/a/10304721/188462
Евгений
1
В Ruby 2.0 есть доработки blog.wyeworks.com/2012/8/3/ruby-refinements-landed-in-trunk
НАРКОЗ
5
Где вы находите oldи redef? Мой 2.0.0 не имеет их. Ах, трудно не пропустить Другие конкурирующие идеи, которые не попали в Ruby:
Накилон
12

Взгляните на методы псевдонимов, это своего рода переименование метода в новое имя.

Для получения дополнительной информации и отправной точки взгляните на эту статью о методах замены (особенно в первой части). Документация по Ruby API также предоставляет (менее сложный) пример.

Veger
источник
-1

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

rplaurindo
источник