2009年07月26日

RubyにもPythonのデコレータを

Pythonのデコレータがとっても便利です。すごくステキ。シンプルな文法のシンタックスシュガーなのに、夢がすごく広がります。僕がデコレータを本格的にさわったきっかけは、TurboGears。ビューのクラスのメソッドに、@exposeとやると、そのメソッドがルーティングテーブルに登録されて、ブラウザからそのメソッド名のURLをたたくと、外から呼び出せるようになる、という仕掛けです。実際に、内部で行われている処理はこんな感じです。

#pythonのデコレータのサンプル
@expose
def exposed_method():
  pass

#こうやって解釈される
def expsed_method():
  pass
exposed_method = expose(exposed_method)

PySpecではテストメソッドの宣言をしたり、wxPythonのユーティリティ関数でイベントハンドラを定義したり(@expose(wx.EVT_BUTTON, "EXIT_BUTTON)みたいな感じで定義できるようにしている)、メソッドに新しい情報を付加したり、関数をラップして処理を割りこませたり、もう、これなしでは設計できないぐらい、いつも便利に使っています。

Rubyにも、デコレータ的なメソッドがあります。publicとかですね。publicと書いたら、その後に定義されたメソッドが全部publicになるという。きっと、「これから登録されるメソッドの公開度はこうしますよ」という情報がクラスの中にあって、それが設定されるような感じかな、と思っています。というわけで、このpublicみたいに、クラス宣言の中で使えて、メソッドに情報を付加するという、デコレータ的な仕組みを実装してみました。サンプルコードとしてはこんな感じです。

class TargetClass
  include DecoratorEnabler # includeするとデコレータ"expose"が使用可能に
  # これは公開しないメソッド
  def not_exposed_method
  end
expose
  # これは公開するメソッド
  def exposed_method
    p "exposed_method()"
  end
end

実現にあたっては、twitter上で、@itawasaさん@shimizukawaさん, @ikasamtさんから参考情報などを教えていただきました。ありがとうございます。

サンプルコードでは使っていませんが、publicとかと違い、メソッドの呼び出し時にパラメータも渡せるようになっています。それでは実装を公開します。

#デコレータを管理するモジュール
#対象となるクラスごとにデコレータの記録を付ける
module Decorators
  @@__decorators = Hash.new {|hash, key| hash[key] = []}
  # デコレータが使用されたら呼ばれるメソッド。次に登録されたメソッドと結びつけられる
  def self.new_decorator(klass, key, args)
    decorators = @@__decorators[klass]
    decorators.pop if decorators.last == nil
    decorators.push(Decorator.new(key, args))
  end
  # メソッドが定義されたら呼ばれるメソッド
  def self.regist_method_to_last_decorator(klass, method)
    decorators = @@__decorators[klass]
    if decorators.last != nil
      decorators.last.regist_method(method)
      # 連続登録を防止するためにnilを入れておいて印にしておく
      decorators.push(nil)
    end
  end
  # デコレータ情報の一覧を取得。デコレータはメソッド名をキーにしたHashで返す
  def self.get_decorators(klass)
    result = Hash.new{|hash, key| hash[key] = []}
    @@__decorators[klass].each { |decorator|
      result[decorator.method_name].push(decorator) if decorator != nil
    }
    result
  end
end

# デコレータ1個の情報を持つクラス。宣言された数だけ生成される。
class Decorator
  attr_reader :decorator, :args, :method_name
  def initialize(decorator, args)
    @decorator = decorator
    @args = args
    @method_name = nil
  end
  def regist_method(method)
    @method_name = method
  end
end

# デコレータを有効にします
module DecoratorEnabler
  def self.included(base)
    base.extend(DecoratorAPI)
  end
  # この中で、メソッドの置き換え、外部への登録などを行う
  # 今はただ画面にデコレータの属性を表示しているだけ
  def initialize
    p Decorators::get_decorators(self.class)
  end
end

# 実際にユーザが使えるデコレータの中身が定義されている
module DecoratorAPI
  def method_added(method)
    Decorators::regist_method_to_last_decorator(self, method)
  end
  # Turbo Gearsライクな、exposeデコレータ(サンプル)
  def expose
    Decorators::new_decorator(self, :expose, [])
  end
end

ちょっとデコレータっぽいことをやるだけでも相当長くなってしまいました。実際に、「自分のプログラムでも取り入れたい」と思われた方が、実際にやるためには、以下のポイントをカスタマイズする必要があります。

  • DecoratorAPIを作って、デコレータの登録部分を必要なだけ追加する
  • DecoratorEnablerのinitializeを書き換え、実際に取得した情報を元に、外部にメソッドの情報を登録するなり、定義されているメソッドを置き換えて別の実装にするなり、色々楽しいことをする

さ、とりあえず、デコレータっぽいことをやるやり方が分かったので、実際にこれを使ったライブラリでも作ってみようかな?もうちょっとがんばって、

@@expose
def exposed_method
  return "hello
end

とかできれば、もうちょっと見た目がデコレータっぽくできるかも。いやぁ、Rubyって難しいわ。

posted by @shibukawa at 17:39 | Comment(127) | TrackBack(1) | Ruby はてなブックマーク - RubyにもPythonのデコレータを
この記事へのトラックバックURL
http://blog.sakura.ne.jp/tb/30839745
※ブログオーナーが承認したトラックバックのみ表示されます。

この記事へのトラックバック

takano32's status on Wednesday, 12-Aug-09 01:30:45 UTC
Excerpt: やっぱり 思考の元っぽい http://blog.shibu.jp/article/30839745.html
Weblog: takano32
Tracked: 2009-08-12 10:30
検索ボックス

Twitter

www.flickr.com
This is a Flickr badge showing public photos and videos from shibukawa.yoshiki. Make your own badge here.
<< 2019年02月 >>
          1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28    
最近の記事
カテゴリ
過去ログ
Powered by さくらのブログ