Когда вы исправляете метод, можете ли вы вызвать переопределенный метод из новой реализации? - PullRequest
410 голосов
/ 17 декабря 2010

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

Например

class Foo
  def bar()
    "Hello"
  end
end 

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

>> Foo.new.bar == "Hello World"

Ответы [ 3 ]

1100 голосов
/ 17 декабря 2010

РЕДАКТИРОВАТЬ : Прошло 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 объектов, например, потому что они созданыс помощью структуры, которая находится вне вашего контроля (например, ), тогда вы можете использовать Pattern Wrapper Design Pattern :

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 (и почти во всех языках ОО на основе одной диспетчеризации) метод привязан к определенному объекту-получателю, который в 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, он будет отображаться во время отражения. Кроме того, его все еще можно вызвать, но, по-видимому, мы исправили его, потому что нам не нравилось его поведение, поэтому мы не хотим, чтобы другие люди вызывали его.

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

от AciveSupport.

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

Если вам нужно другое поведение только в нескольких определенных местах, а не во всей системе, вы можете использовать уточнения, чтобы ограничить патч обезьяны определенной областью. Я собираюсь продемонстрировать это здесь на примере 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 в методе переопределения в 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 в этом предложении.

12 голосов
/ 17 декабря 2010

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

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

0 голосов
/ 19 апреля 2016

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

...