キーワード引数のキーワードをチェックしてみる
fluent interface 流れるようなインターフェイス - #生存戦略 、それは - subtech
Ruby でキーワード引数的な Fluent Interface の実装 - #生存戦略 、それは - subtech
流れるようなインターフェースとメソッドチェーンは違うもの - yvsu pron. yas
最近は流れるようなインターフェースが人気のようだ。
この書き方だと、option がたくさんある時にかっこわるい。
obj.foo(name, :foo => 'bar', :baz => 2)
そっかぁ? けっこう好きなんだけど。
Ruby 1.9だと 「obj.foo(name, foo: 'bar', baz: 2)」 なんて書けるし。
fluent = FluentInterface.fluent WEBrick::HTTPServer, :new server = fluent.DocumentRoot('/var/www').BindAddress('0.0.0.0').Port(8001).execute
強引っぽくて好きじゃないなあ、俺は。「FluentInterface.fluent」とか「.execute」とかが表に出てるのがやだ。
流れるようなインターフェースのかっこいい例はMochaでしょ、やっぱり。「Time.stubs(:now).returns(Time.local(1993,2,24))」と宣言的にスタブを宣言できるから。なんらかを「宣言」する場合に効果抜群のようだ。
Rubyにおけるモックとスタブについて - http://rubikitch.com/に移転しました
Time.nowのテスト? それMochaでできるよ - http://rubikitch.com/に移転しました
Mochaのstub_everythingは強力 - http://rubikitch.com/に移転しました
で、現状の疑似キーワード引数でtypoによるミスを防ぐのなら、こんな風に書けたらいいのではないか。レッツDSL〜
class Super extend AcceptKeywords # :fooを受け付けるmeth1 accept_keywords :foo def meth1(keywords) end # :hogeを受け付けるmeth2 accept_keywords :hoge def meth2(keywords) end def meth3(keywords) end end
継承でキーワードを加える場合はこんな感じ。
class Sub < Super append_keywords :bar def meth1(keywords) keywords end end
で、quick hackな実装を。バグあるかも…
Module#method_addedでメソッド追加をフックするのがミソ。追加されたときに、オリジナルのメソッドにキーワード追加機能を加えたメソッドを再定義する。@__keywordsというクラスのインスタンス変数にメソッド名とキーワードのハッシュを格納しておく。
class Module attr_accessor :__keywords end class KeywordError < StandardError; end module AcceptKeywords def __accept_keywords(keywords, mode) (class << self; self; end).module_eval do added = false define_method(:method_added) do |name| # !> method redefined; discarding old method_added unless added added = true orig = instance_method name if mode == :new superclass_keywords = [] else superowner = self.superclass.instance_method(name).owner superclass_keywords = superowner.__keywords[name] || [] end (self.__keywords ||= {})[name] = keywords + superclass_keywords remove_method name define_method(name) do |*args, &block| hash = args.last if Hash === hash extra_keywords = hash.keys - self.class.__keywords[name] unless extra_keywords.empty? raise KeywordError, "invalid keywords: %p, expects %p" % [ extra_keywords, self.class.__keywords[name] ] end end orig.bind(self).call(*args, &block) end end super name end end end def accept_keywords(*keywords) __accept_keywords keywords, :new end def append_keywords(*keywords) __accept_keywords keywords, :append end end class Super extend AcceptKeywords accept_keywords :foo def meth1(keywords) keywords end accept_keywords :hoge def meth2(keywords) keywords end def meth3(keywords) keywords end end class Sub < Super append_keywords :bar def meth1(keywords) keywords end end class SubSub < Sub append_keywords :gaz def meth2(keywords) keywords end append_keywords :a def meth3(keywords) super end end s = Super.new s.meth1 :foo => 2 # => {:foo=>2} s.meth1 :bar => 3 rescue $! # => #<KeywordError: invalid keywords: [:bar], expects [:foo]> s.meth2 :hoge => 4 # => {:hoge=>4} s.meth2 :boke => 4 rescue $! # => #<KeywordError: invalid keywords: [:boke], expects [:hoge]> s.meth3 :test => 5 # => {:test=>5} s.meth1 7 # => 7 t = Sub.new t.meth1 :foo => 3 rescue $! # => {:foo=>3} t.meth1 :bar => 3 rescue $! # => {:bar=>3} t.meth1 :aar => 3 rescue $! # => #<KeywordError: invalid keywords: [:aar], expects [:bar, :foo]> u = SubSub.new u.meth2 :goge => 4 rescue $! # => #<KeywordError: invalid keywords: [:goge], expects [:gaz, :hoge]> u.meth3 :a => 77 # => {:a=>77}
コメント大歓迎。