Ответ 1
РЕДАКТИРОВАТЬ. Прошло 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
. Это работает еще лучше, если вы используете Шаблон проектирования внедрения зависимостей, Шаблон проектирования метода фабрики, Шаблон проектирования абстрактной фабрики или что-то в этом роде, потому что в В этом случае есть только место, которое нужно изменить.
Делегация
Если вы не контролируете создание объектов Foo
, например, потому что они создаются структурой, которая находится вне вашего контроля (например, ruby-on-rails), тогда вы можете использовать Шаблон дизайна 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, предшествующий деривации
Смешанное наследование (не работает)
Я видел, как некоторые люди пытаются (и спрашивают о том, почему это не работает здесь, на Qaru) что-то вроде этого, то есть 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
вставляет миксин над классом в иерархию наследования, что означает, что 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 (и почти во всех языках ОО на основе одной диспетчеризации) метод связан с конкретным объектом-получателем, который в Ruby называется self
. Другими словами: метод всегда знает, к какому объекту он был вызван, он знает, каков его self
. Но мы взяли метод непосредственно из класса. Откуда он знает, что это за self
?
Что ж, это не так, поэтому нам нужно bind
сначала 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, он будет отображаться во время отражения. Кроме того, он все еще может быть вызван, но, вероятно, мы исправили его, потому что в первую очередь нам не нравилось его поведение, поэтому мы не хотим, чтобы другие люди вызывали его.
Несмотря на то, что это имеет некоторые нежелательные свойства, оно, к сожалению, стало популярным через AciveSupports 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 havent 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 bars 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 bars 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 bars 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
в методе переопределения в prepend
ed mixin, по сути, совпадает с old
в этом предложении.
redef
ключевое слово
Аналогично приведенному выше, но вместо добавления нового ключевого слова для вызова перезаписанного метода и оставления def
одного, мы добавляем новое ключевое слово для переопределения методов. Это обратно совместимо, поскольку синтаксис в настоящее время в любом случае недопустим:
class Foo
def bar
'Hello'
end
end
class Foo
redef bar
old + ' World'
end
end
Foo.new.bar # => 'Hello World'
Вместо добавления двух новых ключевых слов мы также можем переопределить значение super
внутри redef
:
class Foo
def bar
'Hello'
end
end
class Foo
redef bar
super + ' World'
end
end
Foo.new.bar # => 'Hello World'
Замена
Использование redef
метода эквивалентно переопределению метода в prepend
ed mixin. super
в методе переопределения ведет себя как super
или old
в этом предложении.