1. ホーム
  2. スクリプト・コラム
  3. ルビートピックス

Rubyフックメソッド使用例

2022-01-04 06:33:20

フックメソッドを利用することで、Rubyのクラスやモジュールのライフサイクルに介入することができ、プログラミングの自由度を大きく向上させることができます。
ライフサイクルに関連するフックメソッドは以下の通りです。

クラス・モジュール関連

  • クラス#inherited
  • モジュール#include
  • モジュール#prepended
  • モジュール#extend_object
  • モジュール#method_added
  • モジュール#method_removed
  • モジュール#method_undefined

シングルピース・クラス関連

  • BasicObject#singleton_method_addedを指定します。
  • BasicObject#singleton_method_removed
  • BasicObject#singleton_method_undefined

サンプルコード

module M1
  def self.included(othermod)
    puts "M1 was included into #{othermod}"
  end
end

module M2
  def self.prepended(othermod)
    puts "M2 was prepended to #{othermod}"
  end
end

class C
  include M1
  include M2
end

# Output
M1 was included into C
M2 was prepended to C

module M
  def self.method_added(method)
    puts "New method: M##{method}"
  end

  def my_method; end
end

# Output
New method: M#my_method



上記のいくつかの方法に加えて、親クラスのメソッドをオーバーライドして何らかのフィルタリング処理を行い、スーパーメソッドを呼び出して元の関数の機能を完結させることで、フック的な効果を得ることも可能です。このように、エイリアスをラップすることもフックメソッドの代替実装として使用できます。

使用例
タスクの説明

属性の値をチェックするために使用する attr_accessor の attr_checked のような操作を行うクラスマクロを以下のように記述しなさい。

class Person
 include CheckedAttributes

 attr_checked :age do |v|
  v >= 18
 end
end

me = Person.new
me.age = 39 #ok
me.age = 12 #throw exception



実施計画:

eval メソッドを使って、指定されたクラスに単純なチェック付き属性を追加する add_checked_attribute というカーネルメソッドを書いてください。
add_checked_attribute メソッドをリファクタリングして eval メソッドを削除し、他の手段で実装します。
コードブロックチェック機能の追加
add_checked_attribute を attr_checked を必要とするように変更し、すべてのクラスで利用できるようにした。
モジュールを導入することで、機能モジュールを導入するクラスのみにattr_checkedメソッドを追加する
ステップ1

def add_checked_attribute(klass, attribute)
 eval "
  class #{klass}
   def #{attribute}=(value)
    raise 'Invalid attribute' unless value
    @#{attribute} = value
   end
   def #{attribute}()
    @#{attribute}
   end
  end
 "
end

add_checked_attribute(String, :my_attr)
t = "hello,kitty"

t.my_attr = 100
puts t.my_attr

t.my_attr = false
puts t.my_attr



このステップでは eval メソッドを使い、class と def キーワードでそれぞれクラスを開き、指定された属性の get と set メソッドを定義します。set メソッドは単に値が null (nil または false) かどうかを判断し、そうであれば Invalid attribute 例外をスローします。

セットプ 2

def add_checked_attribute(klass, attribute)
 klass.class_eval do
  define_method "#{attribute}=" do |value|
   raise "Invaild attribute" unless value
   instance_variable_set("@#{attribute}", value)
  end

  define_method attribute do
   instance_variable_get "@#{attribute}"
  end

 end
end



このステップでは、eval メソッドを置き換え、さらに class キーワードと def キーワードをそれぞれ class_eval メソッドと define_method メソッドに置き換えています。最初のステップとの違いはなく、いくつかの内部実装の違いがあるだけです。

ステップ3

def add_checked_attribute(klass, attribute, &validation)
 klass.class_eval do
  define_method "#{attribute}=" do |value|
   raise "Invaild attribute" unless validation.call(value)
   instance_variable_set("@#{attribute}", value)
  end

  define_method attribute do
   instance_variable_get "@#{attribute}"
  end

 end
end

add_checked_attribute(String, :my_attr){|v| v >= 180 }
t = "hello,kitty"

t.my_attr = 100 #Invaild attribute (RuntimeError)
puts t.my_attr

t.my_attr = 200
puts t.my_attr #200



派手さはありませんが、コードブロックによる検証を追加したことで、チェックサムに柔軟性が加わり、もはやnilとfalseだけに制限されなくなりました。

ステップ4

class class
 def attr_checked(attribute, &validation)
   define_method "#{attribute}=" do |value|
    raise "Invaild attribute" unless validation.call(value)
    instance_variable_set("@#{attribute}", value)
   end

   define_method attribute do
    instance_variable_get "@#{attribute}"
   end
 end
end

String.add_checked(:my_attr){|v| v >= 180 }
t = "hello,kitty"

t.my_attr = 100 #Invaild attribute (RuntimeError)
puts t.my_attr

t.my_attr = 200
puts t.my_attr #200



なぜなら、すべてのオブジェクトはClassのインスタンスであり、ここで定義されたインスタンスメソッドはRubyの他のすべてのクラスからアクセスすることができるからです。メソッドをattr_checkedに変更しています。

ステップ5

module CheckedAttributes
 def self.included(base)
  base.extend ClassMethods
 end
end

module ClassMethods
 def attr_checked(attribute, &validation)
   define_method "#{attribute}=" do |value|
    raise "Invaild attribute" unless validation.call(value)
    instance_variable_set("@#{attribute}", value)
   end

   define_method attribute do
    instance_variable_get "@#{attribute}"
   end
 end
end

class Person
 include CheckedAttributes

 attr_checked :age do |v|
  v >= 18
 end
end



最後のステップは、CheckedAttributesモジュールが導入された後、フックを介して導入されたモジュールで現在のクラスを拡張し、現在のクラスが導入されたメソッド呼び出し、すなわちここでいうgetとsetのメソッド群をサポートするようにすることである。

この時点で、attr_accessorと同様のattr_checkedというマクロを用意し、属性を任意にチェックできるようにしています。