EDIT : Faz 9 anos desde que escrevi originalmente esta resposta, e ela merece alguma cirurgia estética para mantê-la atualizada.
Você pode ver a última versão antes da edição aqui .
Você não pode chamar o método substituído por nome ou palavra-chave. Essa é uma das muitas razões pelas quais o patch para macacos deve ser evitado e a herança preferida, pois obviamente você pode chamar o método substituído .
Evitando o patch do macaco
Herança
Portanto, se possível, você deve preferir algo assim:
class Foo
def bar
'Hello'
end
end
class ExtendedFoo < Foo
def bar
super + ' World'
end
end
ExtendedFoo.new.bar # => 'Hello World'
Isso funciona, se você controlar a criação dos Foo
objetos. Basta alterar todos os lugares que cria um Foo
para criar um ExtendedFoo
. Isso funciona ainda melhor se você usar o Padrão de Projeto de Injeção de Dependência , o Padrão de Projeto de Método de Fábrica , o Padrão de Projeto de Fábrica Abstrato ou algo nesse sentido, pois, nesse caso, só há um lugar para mudar.
Delegação
Se você não controlar a criação doFoo
objetos, por exemplo, porque eles são criados por uma estrutura que está fora do seu controle (comorubi-sobre-trilhospor exemplo), você pode usar o Padrão de Design do 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'
Basicamente, nos limites do sistema, onde o Foo
objeto entra no seu código, você o envolve em outro objeto e, em seguida, usa esse objeto em vez do original em qualquer outro lugar do código.
Isso usa o Object#DelegateClass
método auxiliar da delegate
biblioteca no stdlib.
Remendo de macaco “limpo”
Os dois métodos acima requerem alteração do sistema para evitar a aplicação de patches em macacos. Esta seção mostra o método preferido e menos invasivo de correção de macacos, caso a alteração do sistema não seja uma opção.
Module#prepend
foi adicionado para suportar mais ou menos exatamente esse caso de uso. Module#prepend
faz a mesma coisa que Module#include
, exceto que ele mistura o mixin diretamente abaixo da classe:
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'
Nota: Eu também escrevi um pouco sobre Module#prepend
esta questão: prefix do módulo Ruby vs derivação
Herança de Mixin (quebrada)
Eu já vi algumas pessoas tentarem (e perguntarem por que não funciona aqui no StackOverflow) algo parecido com isto, isto include
é, misturando em vez de misturar prepend
:
class Foo
def bar
'Hello'
end
end
module FooExtensions
def bar
super + ' World'
end
end
class Foo
include FooExtensions
end
Infelizmente, isso não vai funcionar. É uma boa ideia, porque usa herança, o que significa que você pode usar super
. No entanto, Module#include
insere o mixin acima da classe na hierarquia de herança, o que significa que FooExtensions#bar
nunca será chamado (e, se fosse chamado, super
ele não se referiria realmente, Foo#bar
mas sim ao Object#bar
que não existe), poisFoo#bar
sempre será encontrado primeiro.
Invólucro do método
A grande questão é: como podemos nos apegar ao bar
método, sem realmente manter um método real ? A resposta está, como costuma acontecer, na programação funcional. Nós nos apossamos do método como um objeto real e usamos um fechamento (isto é, um bloco) para garantir que nós e somente nós nos apegemos a esse objeto:
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'
Isso é muito limpo: como old_bar
é apenas uma variável local, ela ficará fora do escopo no final do corpo da classe e é impossível acessá-la de qualquer lugar, mesmo usando reflexão! E como Module#define_method
pega um bloco e fecha o ambiente lexical ao redor (e é por isso que estamos usando em define_method
vez de def
aqui), ele (e somente ele) ainda terá acesso aold_bar
, mesmo depois de sair do escopo.
Breve explicação:
old_bar = instance_method(:bar)
Aqui estamos envolvendo o bar
método em um UnboundMethod
objeto de método e atribuindo-o à variável local old_bar
. Isso significa que agora temos uma maneira de nos manter, bar
mesmo depois de sobrescritos.
old_bar.bind(self)
Isso é um pouco complicado. Basicamente, no Ruby (e em praticamente todas as linguagens OO baseadas em despacho único), um método é vinculado a um objeto receptor específico, chamado self
Ruby. Em outras palavras: um método sempre sabe em qual objeto foi chamado, sabe o que self
é. Mas, pegamos o método diretamente de uma classe, como ele sabe o que self
é?
Bem, isso não acontece, o que é por isso que precisamos bind
nosso UnboundMethod
a um objeto em primeiro lugar, que irá retornar um Method
objeto que pode então chamar. ( UnboundMethod
s não podem ser chamados, porque eles não sabem o que fazer sem conhecer o seu self
.)
E o que fazemos bind
? Simplesmente fazemos bind
isso para nós mesmos, dessa maneira, se comportará exatamente como o original bar
teria!
Por fim, precisamos chamar o Method
que é retornado bind
. No Ruby 1.9, há uma nova sintaxe bacana para isso ( .()
), mas se você estiver no 1.8, pode simplesmente usar o call
método; é para isso que .()
é traduzido de qualquer maneira.
Aqui estão algumas outras perguntas, nas quais alguns desses conceitos são explicados:
Remendo de macaco “sujo”
O problema que estamos enfrentando com o patch de nosso macaco é que, quando sobrescrevemos o método, o método se foi, então não podemos mais chamá-lo. Então, vamos fazer uma cópia de segurança!
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'
O problema disso é que agora poluímos o espaço para nome com um supérfluo old_bar
método . Esse método será exibido em nossa documentação, será exibido na conclusão de código em nossos IDEs, será exibido durante a reflexão. Além disso, ele ainda pode ser chamado, mas, presumivelmente, nós o corrigimos porque, em primeiro lugar, não gostamos de seu comportamento; portanto, talvez não desejemos que outras pessoas o chamem.
Apesar de possuir algumas propriedades indesejáveis, infelizmente se tornou popular através do AciveSupport Module#alias_method_chain
.
Caso você precise apenas do comportamento diferente em alguns locais específicos e não em todo o sistema, use Refinements para restringir o patch do macaco a um escopo específico. Vou demonstrar aqui usando o Module#prepend
exemplo acima:
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!
Você pode ver um exemplo mais sofisticado do uso de refinamentos nesta pergunta: Como ativar o patch de macaco para um método específico?
Idéias abandonadas
Antes da comunidade Ruby se estabelecer Module#prepend
, havia várias idéias diferentes por aí que você pode ver ocasionalmente referenciadas em discussões mais antigas. Todos estes são incluídos em Module#prepend
.
Combinadores de métodos
Uma ideia foi a de combinadores de métodos do CLOS. Esta é basicamente uma versão muito leve de um subconjunto da Programação Orientada a Aspectos.
Usando sintaxe como
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
você seria capaz de "conectar-se" à execução do bar
método.
No entanto, não está claro se e como você obtém acesso ao bar
valor de retorno de dentro bar:after
. Talvez possamos (ab) usar a super
palavra - chave?
class Foo
def bar
'Hello'
end
end
class Foo
def bar:after
super + ' World'
end
end
Substituição
O combinador anterior é equivalente prepend
a um mixin com um método de substituição que chama super
no final do método. Da mesma forma, o combinador posterior é equivalente prepend
a um mixin com um método de substituição que chama super
no início do método.
Você também pode fazer coisas antes e depois da chamada super
, você pode ligar super
várias vezes e recuperar e manipular super
o valor de retorno, tornando prepend
mais poderoso que os combinadores de métodos.
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
e
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
palavra chave
Essa ideia adiciona uma nova palavra-chave semelhante a super
, que permite chamar o método substituído da mesma maneira que super
permite chamar o método substituído :
class Foo
def bar
'Hello'
end
end
class Foo
def bar
old + ' World'
end
end
Foo.new.bar # => 'Hello World'
O principal problema disso é que ele é incompatível com versões anteriores: se você tiver chamado o método old
, não poderá mais chamá-lo!
Substituição
super
em um método de substituição em um prepend
mix ed é essencialmente o mesmo que old
nesta proposta.
redef
palavra chave
Semelhante ao anterior, mas em vez de adicionar uma nova palavra-chave para chamar o método substituído e deixar em def
paz, adicionamos uma nova palavra-chave para redefinir métodos. Isso é compatível com versões anteriores, pois a sintaxe atualmente é ilegal de qualquer maneira:
class Foo
def bar
'Hello'
end
end
class Foo
redef bar
old + ' World'
end
end
Foo.new.bar # => 'Hello World'
Em vez de adicionar duas novas palavras-chave, também podemos redefinir o significado de super
inside redef
:
class Foo
def bar
'Hello'
end
end
class Foo
redef bar
super + ' World'
end
end
Foo.new.bar # => 'Hello World'
Substituição
redef
Ining um método é equivalente a substituir o método em uma prepend
mistura ed. super
no método de substituição se comporta como super
ou old
nesta proposta.