Как расширить модуль Ruby с помощью макроподобных методов метапрограммирования? - PullRequest
6 голосов
/ 11 июня 2010

Рассмотрим следующее расширение (шаблон, распространяемый несколькими плагинами Rails на протяжении многих лет):

module Extension
  def self.included(recipient)
    recipient.extend ClassMethods
    recipient.send :include, InstanceMethods
  end

  module ClassMethods
    def macro_method
      puts "Called macro_method within #{self.name}"
    end
  end

  module InstanceMethods
    def instance_method
      puts "Called instance_method within #{self.object_id}"
    end
  end
end

Если вы хотите показать это каждому классу, вы можете сделать следующее:

Object.send :include, Extension

Теперь вы можете определить любой класс и использовать метод макроса:

class FooClass
  macro_method
end
#=> Called macro_method within FooClass

И экземпляры могут использовать методы экземпляра:

FooClass.new.instance_method
#=> Called instance_method within 2148182320

Но даже если Module.is_a?(Object), вынельзя использовать метод макроса в модуле.

module FooModule
  macro_method
end
#=> undefined local variable or method `macro_method' for FooModule:Module (NameError)

Это верно, даже если вы явно включили исходный Extension в Module с Module.send(:include, Extension).

Для отдельных модулей выможет включать расширения вручную и получать тот же эффект:

module FooModule
  include Extension
  macro_method
end
#=> Called macro_method within FooModule

Но как добавить макросоподобные методы ко всем модулям Ruby?

1 Ответ

22 голосов
/ 11 июня 2010

Рассмотрим следующее расширение (шаблон, популяризированный несколькими плагинами Rails за эти годы)

Это не шаблон, и он не был "популяризирован",Это анти-паттерн , который был обработан грузом 1337 PHP h4X0rZ, который не знает Ruby.К счастью, многие (все?) Экземпляры этого анти-паттерна были устранены из Rails 3 благодаря жесткому слову Иегуды Каца, Карла Лерша и других.Иегуда даже использует почти тот же код, который вы выложили в качестве анти-примера, как в своих недавних выступлениях по очистке кодовой базы Rails, и он написал целый пост в блоге только об этом одном анти-паттерне.

Если вы хотите показать это каждому классу, вы можете сделать следующее:

Object.send :include, Extension

Если вы все равно хотите добавить его в Object , тогда почему бы просто не сделать это:

class Object
  def instance_method
    puts "Called instance_method within #{inspect}"
  end
end

Но как добавить макросоподобные методы ко всем модулям Ruby?

Простой: добавивих на Module:

class Module
  def macro_method
    puts "Called macro_method within #{inspect}"
  end
end

Все это просто работает:

class FooClass
  macro_method
end
#=> Called macro_method within FooClass

FooClass.new.instance_method
#=> Called instance_method within #<FooClass:0x192abe0>

module FooModule
  macro_method
end
#=> Called macro_method within FooModule

Это всего лишь 10 строк кода против ваших 16, и ровно 0 из этих 10 строк являются метапрограммированием илиперехватчики или что-то еще, даже отдаленно сложное.

Единственное различие между вашим кодом и моим заключается в том, что в вашем коде миксины отображаются в иерархии наследования, поэтому отладку немного проще, потому что вы на самом деле см. , что что-то было добавлено к Object.Но это легко исправить:

module ObjectExtensions
  def instance_method
    puts "Called instance_method within #{inspect}"
  end
end

class Object
  include ObjectExtensions
end

module ModuleExtensions
  def macro_method
    puts "Called macro_method within #{inspect}"
  end
end

class Module
  include ModuleExtensions
end

Теперь я связан с вашим кодом в 16 строк, но я бы сказал, что мой код проще, чем ваш, особенно если учесть, что ваш код не работает и ни вы, ни яи почти 190000 пользователей StackOverflow не могут понять, почему.

...