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) щось подібне, тобто 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 метод; ось що .()все-таки перекладається.
Ось пара інших питань, де пояснюються деякі з цих понять:
"Брудні" лапки мавп
Проблема, з якою ми стикаємося з нашим патчем мавп, полягає в тому, що коли ми перезаписуємо метод, метод відсутній, тому ми не можемо його більше називати. Отже, давайте просто зробимо резервну копію!
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в цьому.