読者です 読者をやめる 読者になる 読者になる

(-> % read write unlearn)

All opinions expressed are solely my own and do not express the views or opinions of my employer.

Ruby クラスフック:クラス(モジュール)定義で起きるイベントを掴んで何かする

クラス(やモジュール)定義のイベントの発生時に自動で呼ばれるメソッドが、Rubyには組み込みで用意されています。例えば、「継承」というイベントをhookするメソッドはBasicObject#inherited(klass)です。

inheritedを使って、「このクラスは継承して欲しくないので、継承したら例外を投げる」という実装をしてみます。

class NonInheritable

  def self.inherited(klass)
    super
    raise StandardError, "#{self} is not allowed to be inherited."
  end

  def method_nonInheritable
    puts "#{__method__} is called."
  end

end

class Foo < NonInheritable; end
#=> StandardError: NonInheritable is not allowd to be inherited.
# ・・・(省略)・・・
  • self.inheritedとしてクラスメソッドをオーバーライドしているところに注意してください。
  • inheritedの第一引数には、継承している側(つまり子クラス)のクラスオブジェクトが入ります。
  • 継承されている側(つまり親クラス)のクラスが欲しい場合にはselfを使います。

さて、確かに例外が投げられているのでうまくいっているようです。

しかし、フックはあくまで「〜が起きたときに、・・・する」だけです。上記の例で言えば、あくまで継承時に例外を投げているだけです。継承自体を止められているわけではないです。つまり、この例外をrescueするなりして処理を継続すれば、その後にFoonewすることもできるし、そのnewしたオブジェクトから親クラスであるNonInheritableクラスのメソッドを呼び出すこともできます。

class NonInheritable

  def self.inherited(klass)
    super
    raise StandardError, "#{self} is not allowed to be inherited."
  end

  def method_nonInheritable
    puts "#{__method__} is called."
  end

end

begin
  class Foo < NonInheritable; end
rescue => e
  puts e.message
end
#=> NonInheritable is not allowed to be inherited.


Foo.ancestors
#=> [Foo, NonInheritable, Object, Kernel, BasicObject]

Foo.new.method_nonInheritable
#=> method_nonInheritable is called.

inheritedのような、クラス(やモジュール)定義のイベントをフックするメソッドは、全部で7つあります。*1

inheritedのみBasicObjectクラスで定義されていて、他はKernelモジュールで定義されているようです。

irb(main):069:0> RUBY_VERSION
=> "2.2.3"

irb(main):070:0> Object.private_methods(false).grep /ed$/
=> [:inherited]
irb(main):071:0> Kernel.private_methods(false).grep /ed$/  # :protectedはフックとは関係ないメソッド。無視してください。。
=> [:included, :extended, :prepended, :method_added, :method_removed, :method_undefined, :protected]

整理してみます

メソッド フックポイント 注意
inherited(child) クラスが継承されたとき 第一引数は、継承する側のクラス(つまり子クラス)。
extended(obj) モジュールがオブジェクトに取り込まれた(extendされた)とき 第一引数は、extendする側のオブジェクト。
included(mod) モジュールがincludeされたとき 第一引数は、includeする側のモジュール。
prepended(mod) モジュールがprependされたとき 第一引数は、prependする側のモジュール。
Ruby2.0で導入。
method_added(name) クラスやモジュールでメソッドが定義されたとき 第一引数は、追加されたメソッド名のシンボル。
method_removed(name) クラスやモジュールでメソッドが削除(remove_method)されたとき 第一引数は、削除されたメソッド名のシンボル。
method_undefined(name) クラスやモジュールでメソッドが未定義に(undef_method)されたとき 第一引数は、未定義にされたメソッド名のシンボル。

全部使ってみます

# フック時に表示するメッセージのテンプレート
MESSAGE = "%09s has %09s %-s"

# クラスにフックを定義
class Parent
  def self.inherited(klass);       puts MESSAGE % [self, __method__, klass] end  # クラスが継承されたとき
end

# モジュールにフックを定義
module Mod
  def self.extended(obj);          puts MESSAGE % [self, __method__,   obj] end  # モジュールがオブジェクトに取り込まれた(extend)されたとき
  def self.included(mod);          puts MESSAGE % [self, __method__,   mod] end  # モジュールがincludeされたとき
  def self.prepended(mod);         puts MESSAGE % [self, __method__,   mod] end  # モジュールがprependされたとき
  def self.method_added(name);     puts MESSAGE % [self, __method__,  name] end  # メソッドが定義されたとき
  def self.method_removed(name);   puts MESSAGE % [self, __method__,  name] end  # remove_methodによってメソッドが削除されたとき
  def self.method_undefined(name); puts MESSAGE % [self, __method__,  name] end  # undef_methodによってメソッドが未定義にされたとき
end



class Child < Parent
  def to_s; "child" end
end
# inherited 発動

child = Child.new
child.extend(Mod)  # extended 発動

module SubMod
  include Mod  # included 発動
  prepend Mod  # prepended 発動
end

module Mod
  def foo; end  # method_added 発動
  def bar; end  # method_added 発動

  remove_method :foo  # method_removed 発動

  undef_method :bar  # method_undefined 発動
end
  • このコードでは端折ってますが、フックを使う場合は基本的にsuperを呼び出すのが行儀いいです。

参考

*1:prependedがRuby2.0で新たに導入された例もあり、今後増える可能性は大いにあります。