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
було додано для підтримки більш-менш точно цього випадку використання. 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) щось подібне, тобто include
ing mixin замість prepend
ing:
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, що базується на одній диспетчеризації) метод пов'язаний з конкретним об'єктом приймача, який називається self
Ruby. Іншими словами: метод завжди знає, який об’єкт був викликаний, він знає, що це self
таке. Але ми схопили метод безпосередньо з класу, як він знає, що це self
таке?
Ну, це не так, саме тому нам потрібно спочатку bind
наш UnboundMethod
об'єкт, який поверне Method
об'єкт, який ми можемо потім викликати. ( UnboundMethod
їх не можна викликати, оскільки вони не знають, що робити, не знаючи їх self
.)
І що ми bind
це робимо ? Ми просто bind
це самі собі, таким чином він буде вести себе так, як bar
мав би оригінал !
Нарешті, нам потрібно зателефонувати до того, Method
що повернуто з bind
. У Ruby 1.9 є новий чудовий синтаксис для цього ( .()
), але якщо ви перебуваєте на 1.8, ви можете просто використовуватиcall
метод; ось що .()
все-таки перекладається.
Ось пара інших питань, де пояснюються деякі з цих понять:
"Брудні" лапки мавп
Проблема, з якою ми стикаємося з нашим патчем мавп, полягає в тому, що коли ми перезаписуємо метод, метод відсутній, тому ми не можемо його більше називати. Отже, давайте просто зробимо резервну копію!
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
в цьому.