Ao aplicar um patch a um método de instância, você pode chamar o método substituído da nova implementação?

442

Digamos que eu esteja corrigindo um método em uma classe, como eu poderia chamar o método substituído pelo método de substituição? Ou seja, algo um pouco comosuper

Por exemplo

class Foo
  def bar()
    "Hello"
  end
end 

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

>> Foo.new.bar == "Hello World"
James Hollingworth
fonte
A primeira classe Foo não deveria ser outra e a segunda Foo herdaria dela?
Draco Ater
1
Não, eu sou macaco remendo. Eu esperava que houvesse algo como super () que eu pudesse usar para chamar o método original #
James Hollingworth
1
Isso é necessário quando você não controla a criação Foo e o uso de Foo::bar. Então você tem que corrigir o método.
Halil Özgür

Respostas:

1165

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 Fooobjetos. Basta alterar todos os lugares que cria um Foopara 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 (comopor 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 Fooobjeto 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#DelegateClassmétodo auxiliar da delegatebiblioteca no stdlib.

Remendo de macaco “limpo”

Module#prependAnterior: Mixin Prepending

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#prependfoi adicionado para suportar mais ou menos exatamente esse caso de uso. Module#prependfaz 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#includeinsere o mixin acima da classe na hierarquia de herança, o que significa que FooExtensions#barnunca será chamado (e, se fosse chamado, superele não se referiria realmente, Foo#barmas sim ao Object#barque não existe), poisFoo#bar sempre será encontrado primeiro.

Invólucro do método

A grande questão é: como podemos nos apegar ao barmé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_methodpega um bloco e fecha o ambiente lexical ao redor (e é por isso que estamos usando em define_methodvez de defaqui), 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 barmétodo em um UnboundMethodobjeto de método e atribuindo-o à variável local old_bar. Isso significa que agora temos uma maneira de nos manter, barmesmo 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 selfRuby. 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 bindnosso UnboundMethoda um objeto em primeiro lugar, que irá retornar um Methodobjeto que pode então chamar. ( UnboundMethods 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 bindisso para nós mesmos, dessa maneira, se comportará exatamente como o original barteria!

Por fim, precisamos chamar o Methodque é retornado bind. No Ruby 1.9, há uma nova sintaxe bacana para isso ( .()), mas se você estiver no 1.8, pode simplesmente usar o callmé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”

alias_method cadeia

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.

Um aparte: Refinamentos

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#prependexemplo 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 barmétodo.

No entanto, não está claro se e como você obtém acesso ao barvalor de retorno de dentro bar:after. Talvez possamos (ab) usar a superpalavra - chave?

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  def bar:after
    super + ' World'
  end
end

Substituição

O combinador anterior é equivalente prependa um mixin com um método de substituição que chama superno final do método. Da mesma forma, o combinador posterior é equivalente prependa um mixin com um método de substituição que chama superno início do método.

Você também pode fazer coisas antes e depois da chamada super, você pode ligar supervárias vezes e recuperar e manipular supero valor de retorno, tornando prependmais 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 superpermite 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

superem um método de substituição em um prependmix ed é essencialmente o mesmo que oldnesta 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 defpaz, 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 superinside redef:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    super + ' World'
  end
end

Foo.new.bar # => 'Hello World'

Substituição

redefIning um método é equivalente a substituir o método em uma prependmistura ed. superno método de substituição se comporta como superou oldnesta proposta.

Jörg W Mittag
fonte
@ Jörg W Mittag, o thread de abordagem de quebra de método é seguro? O que acontece quando dois threads simultâneos chamam bindna mesma old_methodvariável?
Harish Shetty
1
@KandadaBoggu: Estou tentando descobrir exatamente o que você quer dizer com isso :-) No entanto, tenho certeza de que não é menos seguro para threads do que qualquer outro tipo de metaprogramação em Ruby. Em particular, cada chamada para UnboundMethod#bindretornará um novo, diferente Method, portanto, não vejo nenhum conflito surgindo, independentemente de você chamá-lo duas vezes seguidas ou duas ao mesmo tempo em threads diferentes.
Jörg W Mittag
1
Estava procurando uma explicação sobre o patch assim desde que comecei no ruby ​​and rails. Ótima resposta! A única coisa que faltava para mim era uma nota sobre class_eval vs. reabrir uma classe. Aqui está: stackoverflow.com/a/10304721/188462
Eugene
1
O Ruby 2.0 possui aprimoramentos blog.wyeworks.com/2012/8/3/ruby-refinements-landed-in-trunk
NARKOZ
5
Onde você encontra olde redef? Meu 2.0.0 não os possui. Ah, é difícil não perder as outras idéias concorrentes que não
apareceram
12

Dê uma olhada nos métodos de alias, isso é meio que renomear o método para um novo nome.

Para obter mais informações e um ponto de partida, consulte este artigo sobre métodos de substituição (especialmente a primeira parte). Os documentos da API Ruby também fornecem um exemplo (menos elaborado).

Veger
fonte
-1

A classe que fará a substituição deve ser recarregada após a classe que contém o método original, portanto require, no arquivo que fará a substituição.

rplaurindo
fonte