カテゴリー:
Ruby
タグ:
 Ruby クラスマクロ クラスメソッド まとめ メタプログラミング

このエントリーをはてなブックマークに追加
更新日時:
2013年05月31日(金)
作成日時:
2013年05月30日(木)

前の記事 / 次の記事

クラスマクロって何?

例えば

class Book
  attr_accessor :title, :author, :isbn
end

みたいなやつ。

で、これはそもそも何なのかというと ただのクラスメソッド

クラスメソッドとして attr_accessor(:title, :author, :isbn) と呼び出すところを
括弧を省略して attr_accessor :title, :author, :isbn と書いているだけ。

なので attr_accessor のようなクラスマクロをつくりたい場合、
クラスメソッドを定義すればいい

まつもと直伝 プログラミングのオキテ 第6回 を参考にすると、
attr_accessor をRubyで書くなら次のようなクラスメソッドになる(コードは引用元とは違う)。
(%Q| | を使っているのは単なる趣味、 " " と同義。)

# *の付いた引数は、余った引数を配列として受け取る
# この場合は全ての引数を配列として受け取る
def self.attr_accessor(*attrs)
  attrs.each do |attr|

    # Getter
    define_method attr do
      instance_variable_get(%Q|@#{attr}|)
    end

    # Setter
    define_method %Q|#{attr}=| do |val|
      instance_variable_set(%Q|@#{attr}|, val)
    end

  end
end

このクラスメソッドにパラメーターとして属性名を渡す事で自動的にアクセサが生成される。

で、クラスマクロにしたいと思うくらいだから当然処理を共有したいはずなので
共有の仕方によって次の5つの書き方を考えてみた。

  1. 直接クラスメソッドとして書く
  2. 特異メソッドとして書く
  3. extend されるモジュールとして書く
  4. include されるモジュールとして書く
  5. module を拡張して書く
  6. 参考ページ
  7. 参考書籍

ここでは名前の衝突を避けるためにそれぞれ、
attr_accessor_direct, attr_accessor_singleton,
attr_accessor_extend, attr_accessor_include, attr_accessor_module
というメソッドを作成する。

1. 直接クラスメソッドとして書く

最初に書いた方法。
クラスを継承して使うような場合。

attr_accessor_directメソッドをMediaクラスが持つべきなのかっていうのは考えない方向で。

class Media
  def self.attr_accessor_direct(*attrs)
    attrs.each do |attr|

      define_method attr do
        instance_variable_get(%Q|@#{attr}|)
      end

      define_method %Q|#{attr}=| do |val|
        instance_variable_set(%Q|@#{attr}|, val)
      end

    end
  end
end

class Book < Media
  attr_accessor_extend :title, :author, :isbn
end

2. 特異メソッドとして書く

基本的には1.の方法と同じ。
なぜならクラスの特異メソッドとクラスメソッドは同じものだから。

該当のクラス内にクラスメソッドの定義を書かなくても、
そのクラスメソッドが必要になったタイミングでクラスメソッドを追加出来る。

Bookクラスに直接実装すればいいじゃんっていうのは考えない方向で。
特異メソッドとしてクラスマクロを実装出来るという出来の悪い例とうことで。

class Media
end

class Book < Media

  class << Media
    def attr_accessor_singleton(*attrs)
      attrs.each do |attr|

        define_method attr do
          instance_variable_get(%Q|@#{attr}|)
        end

        define_method %Q|#{attr}=| do |val|
          instance_variable_set(%Q|@#{attr}|, val)
        end

      end
    end
  end

  attr_accessor_extend :title, :author, :isbn
end

3. extend されるモジュールとして書く

一番素直な方法?
クラスマクロを利用したいクラスで extend。

module AccessorExtend
  def attr_accessor_extend(*attrs)
    attrs.each do |attr|

      define_method attr do
        instance_variable_get(%Q|@#{attr}|)
      end

      define_method %Q|#{attr}=| do |val|
        instance_variable_set(%Q|@#{attr}|, val)
      end

    end
  end
end

class Book
  extend AccessorExtend
  attr_accessor_extend :title, :author, :isbn
end

4. include されるモジュールとして書く

何らかの理由で extend 出来ないか include して使いたい場合。
クラスマクロを利用したいクラスで include。

include した場合、クラスメソッドは include 元の class_eval の文脈で定義する必要がある。

included はインクルードされた時に実行されるメソッド。
base にはインクルード元のクラスが渡る。

base.class_eval {} でインクルード元の class_eval の文脈で定義出来る。

module AccessorInclude
  def self.included(base)
    base.class_eval {
      def self.attr_accessor_include(*attrs)
        attrs.each do |attr|

          define_method attr do
            instance_variable_get(%Q|@#{attr}|)
          end

          define_method %Q|#{attr}=| do |val|
            instance_variable_set(%Q|@#{attr}|, val)
          end

        end
      end
    }
  end
end

class Book
  include AccessorInclude
  attr_accessor_extend :title, :author, :isbn
end

または、特異メソッドと組み合わせて include する。
クラスメソッドとして include するメソッドを定義するってことだから extend と同じ。
あるのか分からないけどどうしても extend も class_eval もしたくない時に使える??

module AccessorInclude
  def attr_accessor_extend(*attrs)
    attrs.each do |attr|

      define_method attr do
        instance_variable_get(%Q|@#{attr}|)
      end

      define_method %Q|#{attr}=| do |val|
        instance_variable_set(%Q|@#{attr}|, val)
      end

    end
  end
end

class Book
  class << self
    include AccessorInclude
  end

  attr_accessor_extend :title, :author, :isbn
end

5. module の拡張として書く

組み込みのメソッドのように使いたい場合。
Moduleクラスをオープンしてメソッドを定義する。

まつもと直伝 プログラミングのオキテ 第6回 に書かれていた方法。

class Module
  def attr_accessor_module(*attrs)
    attrs.each do |attr|
      class_eval {
        define_method attr do
          instance_variable_get(%Q|@#{attr}|)
        end

        define_method %Q|#{attr}=| do |val|
          instance_variable_set(%Q|@#{attr}|, val)
        end
      }
    end
  end
end

6. 参考ページ

7. 参考書籍