2013年10月29日火曜日

Rubyでクラスマクロを実装


Rubyの動的言語の強みを最大限に生かした方法で
クラスマクロを種々の方法で実装していく。

以下読み進めるにあたって、
Rubyのメタプログラミング技術は理解できていることを前提とする。


クラスマクロは知っているだろう。
クラス定義内で使える以下のようなクラスメソッドである。
class MyClass
  attr_accessor :my_attribute
end


ここでは以下を実現するクラスメソッドを手段を変えて例示していく。

クラスメソッド内容
属性値を検証する機能もったクラスメソッドを作り、
ついでにゲッターとセッターのメソッドを作る。



◆ クラス内に直接クラスメソッドを記載方式
class Person
  def self.attr_checked(attribute, &validation)

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

    # setter
    define_method "#{attribute}=" do |value|
      raise 'Invalid attribute' unless validation.call(value)
      instance_variable_set("@#{attribute}", value)
    end
  end

  # :ageとdo以降のブロックが2つの引数となる
  attr_checked :age do |value|
    value >= 20
  end
end


person = Person.new
person.age = 30 # => 30
person.age = 10 # => raiseのエラーが返る



◆ extend方式
module CheckedAttributes
  def attr_checked(attribute, &validation)

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

    # setter
    define_method "#{attribute}=" do |value|
      raise 'Invalid attribute' unless validation.call(value)
      instance_variable_set("@#{attribute}", value)
    end

  end
end


class Person
  extend CheckedAttributes

  attr_checked :age do |value|
    value >= 20
  end
end



◆ 特異メソッド方式
module CheckedAttributes
  def attr_checked(attribute, &validation)

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

    # setter
    define_method "#{attribute}=" do |value|
      raise 'Invalid attribute' unless validation.call(value)
      instance_variable_set("@#{attribute}", value)
    end

  end
end


class Person
  class << self
    include CheckedAttributes
  end

  attr_checked :age do |value|
    value >= 20
  end
end



◆ moduleの拡張方式
module CheckedAttributes
  def self.included(includer)
    includer.extend ClassMethods
  end

  module ClassMethods
    def attr_checked(attribute, &validation)

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

      # setter
      define_method "#{attribute}=" do |value|
        raise 'Invalid attribute' unless validation.call(value)
        instance_variable_set("@#{attribute}", value)
      end

    end
  end
end


class Person
  include CheckedAttributes

  attr_checked :age do |value|
    value >= 20
  end
end



◆ include方式
module CheckedAttributes
  def self.included(includer)
    includer.class_eval do
      def self.attr_checked(attribute, &validation)
        # getter
        define_method attribute do
          instance_variable_get "@#{attribute}"
        end

        # setter
        define_method "#{attribute}=" do |value|
          raise 'Invalid attribute' unless validation.call(value)
          instance_variable_set("@#{attribute}", value)
       end

     end
  end
end


class Person
  include CheckedAttributes

  attr_checked :age do |value|
    value >= 20
  end
end



◆ 全クラスオブジェクトに追加方式
module CheckedAttributes
  def attr_checked(attribute, &validation)

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

    # setter
    define_method "#{attribute}=" do |value|
      raise 'Invalid attribute' unless validation.call(value)
      instance_variable_set("@#{attribute}", value)
    end

  end
end


Class.send(:include, CheckedAttributes)


class Person
  attr_checked :age do |value|
    value >= 20
  end
end



◆◆◆◆
最後は実装手段ではなく、便利そうなクラスマクロがあったので
利活用の観点で記録しておく。

とあるクラスのメソッド名を変更したい。
ただし古いメソッド名を突然使えなくするのではなく、
警告を出して新しいメソッドへの利用を促すソフトな処置をとりたい。
class Person
  def initialize(age)
    @age = age
  end

  def age
    @age
  end

  def self.deprecate(old_method, new_method)
    define_method(old_method) do |*args|
      warn "Warning: #{old_method}() is deprecated. Use #{new_method}()."
      send(new_method, *args)
    end
  end

  deprecate :getAge, :age
end

person = Person.new(20)
person.getAge