Коли мавпа виправляє метод екземпляра, чи можете ви викликати перезаписаний метод з нової реалізації?


444

Скажіть, що я мавпа, що виправляє метод у класі, як я можу викликати метод, що переосмислюється, від методу, що перекриває? Тобто щось на кшталт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. Тож вам доведеться мавпочку виправити метод.
Halil Özgür

Відповіді:


1165

EDIT : Минуло цю відповідь вже 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. Це працює навіть краще, якщо ви використовуєте шаблон дизайну інжекційних залежностей , Factory Method Design Pattern , то шаблон 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: Mixin попередньо

Два вищевказані методи вимагають змінити систему, щоб уникнути виправлення мавп. У цьому розділі показаний кращий і найменш інвазивний метод виправлення мавп, якщо зміна системи не є можливим.

Module#prependбуло додано для підтримки більш-менш точно цього випадку використання. Module#prependробить те саме, що Module#include, за винятком того, що він змішується в mixin безпосередньо під класом:

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 передбачати проти виведення

Спадщина Міксіна (зламана)

Я бачив, як деякі люди намагаються (і запитують, чому це не працює тут на StackOverflow) щось подібне, тобто includeing mixin замість prepending:

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 (і майже в усіх мовах OO, що базується на одній диспетчеризації) метод пов'язаний з конкретним об'єктом приймача, який називається selfRuby. Іншими словами: метод завжди знає, який об’єкт був викликаний, він знає, що це 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, він відобразиться під час роздумів. Крім того, його все ще можна назвати, але, мабуть, ми мавпи зафіксували це, оскільки нам не сподобалася в першу чергу його поведінка, тому ми можемо не хотіти, щоб інші люди називали це.

Незважаючи на те, що це має деякі небажані властивості, воно, на жаль, стало популяризовано через 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!

Ви можете побачити більш складний приклад використання уточнень у цьому запитанні: як увімкнути патч мавпи для конкретного методу?


Кинуті ідеї

Перед тим, як спільнота Рубі влаштувалась 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

Заміна

Попередній комбінатор рівносильний prependміксину з переважаючим методом, який викликає superв самому кінці методу. Аналогічно, комбінатор after є еквівалентним 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ед-міксіні по суті такий же, як 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ед-міксині. superу переважаючому методі поводиться як у цій пропозиції, так superі oldв цьому.


@ Jörg W Mittag, чи безпечна нитка підходу для обгортання методу безпечна? Що відбувається, коли дві одночасні нитки викликають bindодну і ту ж old_methodзмінну?
Харіш Шетті

1
@KandadaBoggu: Я намагаюся зрозуміти, що саме ти маєш на увазі під цим :-) Однак я впевнений, що це не менш безпечно для потоків, ніж будь-який інший тип метапрограмування в Ruby. Зокрема, кожен заклик до UnboundMethod#bindповертає новий, інший Method, тож я не бачу виникнення конфлікту, незалежно від того, ви називаєте його двічі поспіль або два рази одночасно з різних потоків.
Йорг W Міттаг

1
Шукав пояснення щодо такого виправлення, як це було з моменту запуску на рубіні та рейках. Чудова відповідь! Єдине, чого мені не вистачало, - це записка про class_eval vs. повторне відкриття класу. Ось це: stackoverflow.com/a/10304721/188462
Євген


5
Де ви знаходите oldі redef? Мій 2.0.0 їх не має. Ах, важко не пропустити Інші конкуруючі ідеї, які не перетворилися на Рубі, були:
Накілон

12

Погляньте на сповіщення методів, це свого роду перейменування методу на нову назву.

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


-1

Клас, який зробить переопределення, повинен бути перезавантажений після класу, який містить оригінальний метод, так що requireце у файлі, який зробить переозначення.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.