ぱろっと・すたじお

技術メモなどをまったりと / my site : http://parrot-studio.com/

RubyでDSLっぽいことをする時の基本のような何か

例によって自分用のメモ書きなのですが、
強気なタイトルに出られないのは、自分の中で確信が持てないからで・・・


とはいえ、実際に仕事で使っているテクニックですし、
使おうとするたびに毎回調べたり検証コードを書いたりが面倒なので、
一度まとめておきます...φ(・ω・`)

「DSL的に書く」とは?


先日の「Gunma.web」*1でもRubyに対して「黒魔術」という表現を使いましたが、
例えばこんなDSL的なコードで細かい挙動を変えられる、というのはその一端です

class User < ActiveRecord::Base
  validates :user_id, :presence => true
  # 厳密に書こうとするとこうなる
  validates(:user_name, {:presence => true})
end


この場合の「validates」は単なるクラスメソッドで、
引数の「()」が省略でき、最後の引数がHashの場合は「{}」を省略できるという、
Rubyのメソッド呼び出しの仕様を利用しているだけです


でも、「それだけのこと」で、高い表現力を実現しているのが、
Rubyのすごいところではありますが、それはさておいて・・・


逆に、このようなDSL的な表現を自分で書くにはどうしたらいいのでしょう(´・ω・)?


つまり、最終的にこんなのが書きたいわけです

class Reply < Comment
  comment_type :reply # これをキーにしてクラスの挙動を変える
end


この場合、Replyの親クラスであるCommentクラスの実装が問題になります
言い換えると、以下をどう実装するか、ということです

class Comment

  class << self
    # クラスメソッド
    def comment_type(t)
      # 何らかの実装
    end
  end

  # インスタンスメソッド
  def view_comment_type
    # 何らかの実装
  end

end


「comment_type」というメソッドは「クラスの定義」を表しているので、
インスタンスではなく)クラス自身がその情報をどう抱えるか、
その手法がポイントになります

とりあえずクラス変数を使ってみる


Rubyのクラス変数には罠が多くて、
覚えたての頃は多用してましたが、今は一切使ってませんΣ(・ω・ノ)ノ


とはいえ、「クラス自身が情報を持つ」という言葉から、
最初に思いつくのはクラス変数でしょう


というわけで、まずクラス変数で書いてみます(´・ω・)っ

class Comment

  class << self
    def comment_type(t)
      @@comment_type = t
    end
  end

  def view_comment_type
    @@comment_type
  end

end

class Reply < Comment
  comment_type :reply
end

rep = Reply.new
p rep.view_comment_type #=> :reply


おお、要件を満たしていますヽ(`・ω・´)ノ


しかし、こうしたらどうでしょう?

# (前略)
p rep.view_comment_type #=> :reply

class Tweet < Comment
  comment_type :tweet
end

tw = Tweet.new
p tw.view_comment_type  #=> :tweet

p rep.view_comment_type #=> :tweet


なんと、後から定義したクラスによって、
元々のReplyクラスの挙動が変わってしまいましたΣ(゚Д゚)ガーン


「@@comment_type」というクラス変数は、Commentに属した単一の情報なので、
後から定義した情報で上書きされてしまうのです*2
これでは使えません

クラス自身のインスタンス変数に隠す


ところで、Rubyにおける「クラス」というのは、
「Classクラスのインスタンスを定数に結びつけたもの」です


擬似的に書けばこういうことです

class Class
end

rep = Class.new
Reply = rep


で、「クラス」もインスタンスの一種なのであれば、
当然ながら「インスタンス変数」を定義することができます


なので、こういう書き方ができます(´・ω・)っ

class Comment

  class << self
    def comment_type(t)
      @comment_type = t
    end
    
    def view_comment_type
      @comment_type
    end
  end

  def view_comment_type
    # クラスメソッドのview_comment_typeに委譲
    self.class.view_comment_type
  end

end

class Reply < Comment
  comment_type :reply
end

rep = Reply.new
p rep.view_comment_type #=> :reply

class Tweet < Comment
  comment_type :tweet
end

tw = Tweet.new
p tw.view_comment_type  #=> :tweet

p rep.view_comment_type #=> :reply


結果的にこれでうまくいくのですが・・・わけがわからないよΣ(・ω・ノ)ノ


そもそも「class << self」の「self」ってなにを指しているのでしょう?
ちょっとコードに細工してみます

class Comment

  class << self
    def comment_type(t)
      p self # (1)
      @comment_type = t
    end
    
    def view_comment_type
      @comment_type
    end
  end

  def view_comment_type
    p self # (2)
    p @comment_type # (3) 
    self.class.view_comment_type
  end

end

class Reply < Comment
  comment_type :reply
end

rep = Reply.new
p rep.view_comment_type # (4)

# 出力結果
Reply # (1)
#<Reply:0x00000...> # (2)
nil # (3)
:reply # (4)


順に見ていくと、まず(1)のselfが指しているのは、
「Classクラスのインスタンスである、とあるオブジェクト」です
それを定数に代入したのが「Replyクラス」です


つまり、擬似的にはこう書いたのと同じことです

class Class

  def comment_type(t)
   @comment_type = t
 end

  def view_comment_type
    @comment_type
  end

end

rep1 = Class.new
rep1.comment_type = :reply
rep1.view_comment_type #=> :reply

Reply = rep1
Reply.view_comment_type #=> :reply

rep2 = Class.new
rep2.comment_type = :tweet
rep2.view_comment_type #=> :tweet

Tweet = rep2
Tweet.view_comment_type #=> :tweet


「@comment_type」は「インスタンス変数」なので、
インスタンスごとに値が違っていい」わけです
なので、「ReplyというインスタンスとTweetというインスタンスで値が違う」のは当然です


続いて、(2)のselfはすぐわかりますね
「Replyクラスのインスタンス」を指します
(3)は(1)とselfが違う=文脈が違うので、nilが返ります



このように、「純粋なオブジェクト指向言語」であるRubyは、
一見「黒魔術」に見える動作であっても、
実は単なる仕様の組み合わせだったりします


「self」がキーになることが多い*3ので、
わからなくなった場合は「selfが何を指しているか?」を
意識してみるといいかもしれません*4


<おまけ1>
格納場所や手順を複雑化しただけで、
「書き換えができない」わけではないことに注意

# (前略)
rep1 = Reply.new
p rep1.view_comment_type #=> :reply

rep2 = Reply.new
p rep2.view_comment_type #=> :reply

rep1.class.class_eval do
  # 強引にインスタンス変数を書き換える
  @comment_type = :hoge
end

p rep1.view_comment_type #=> :hoge
p rep2.view_comment_type #=> :hoge


クラスが抱える情報そのものを書き換えたので、
両方のオブジェクトで値が変わってしまいます


まあ、こういうところが良くも悪くもRubyなのですが(´-ω-)


<おまけ2>
定義情報を取り出すだけのメソッドがpublicなクラスメソッドにあるのは気持ち悪いし、
privateにして隠蔽したいので、私はこんな書き方をすることが多いです(´・ω・)っ

class Comment

  class << self
    def comment_type(t)
      @comment_type = t
    end
  end

  def some_method
    # comment_typeを使うメソッド
  end

  private

  def view_comment_type
    t = nil
    self.class.class_eval do
      # p self #=> Reply
      t = @comment_type
    end
    t
  end

end


頻繁に呼び出すならこんなんとか

class Comment

  class << self
    def comment_type(t)
      @comment_type = t
    end
  end

  private

  def view_comment_type
    @comment_type ||= lambda do
      t = nil
      self.class.class_eval do
        # p self #=> Reply
        t = @comment_type
      end
      t
    end.call
    # なくても結果は同じだけど、ラストで返り値をを明示する方が好み
    @comment_type
  end

end


後者は「遅延初期化と値のキャッシュ」的なテクニックなので、
いろいろな場合に使えますが、
「値が変わる場合」に使うと面倒なことに


ここまで読むような方には釈迦に説法でしょうけども(´・ω・`)

*1: http://d.hatena.ne.jp/parrot_studio/20111220/1324391867

*2: 継承が絡むとわかりづらくなる、という点で、私はクラス変数を使わないようにしています

*3: オブジェクト指向言語の大半がそうとも言う

*4: 例えば、JavaScriptで「this」が何を指しているか、と同じような感覚

広告を非表示にする