「DSLヒッチハイク・ガイド」で使用したコード
Ruby勉強会@札幌-8で、「RubyでDSLを作成する際の作戦の立て方と表現のポイント」について発表した際に、使用したコードです。お題は「アンケート作成を支援するDSL」。
DSLじゃない実現
まず始めに、OOPで普通に実現したサンプル。ERBテンプレートはかなり適当なことになっていますが、スルーしてください。
#!/user/bin/ruby -Ku require 'erb' class Questionnaire TEMPLATE = <<EOP <b><%= context.title %></b><br/> <% context.questions.each do |q| %> <p><%= q.title %></p> <% q.answers.each do |a| %> <li><%= a %></li> <% end %> <% end %> EOP def self.create_html(context) puts ERB.new(TEMPLATE).result(binding) end end # ValueObject class Context attr_accessor :title, :questions def initialize title, questions @title = title @questions = questions end end class Question attr_accessor :title, :answers def initialize title, answers @title = title @answers = answers end end # Non-DSL sample ctx = Context.new("Ruby勉強会-8のアンケート",[]) ctx.questions << Question.new("勉強会への参加は初めてですか?", ["初めて", "常連"]) ctx.questions << Question.new("今回の勉強会はどうでしたか?", ["良かった", "まあまあ", "最低"]) ctx.questions << Question.new("次回も参加したいですか?", ["したい", "内容によって", "二度とくるか"]) Questionnaire.create_html(ctx)
グローバル・メソッドを使って
グローバル・メソッドを使用したDSLのサンプル。
#!/user/bin/ruby -Ku require "erb" # ValueObject class Context attr_accessor :title, :questions def initialize title, questions @title = title @questions = questions end end class Question attr_accessor :title, :answers def initialize title, answers @title = title @answers = answers end end TEMPLATE = <<EOP <b><%= @target.title %></b><br/> <% @target.questions.each do |q| %> <p><%= q.title %></p> <% q.answers.each do |a| %> <li><%= a %></li> <% end %> <% end %> EOP def context text @target = Context.new(text, []) yield puts ERB.new(TEMPLATE).result(binding) end def question text q = Question.new(text, []) yield q @target.questions << q end # DSL sample context "Ruby勉強会-8のアンケート" do question "勉強会への参加は初めてですか?" do |q| q.answers << "初めて" q.answers << "常連" end question "今回の勉強会はどうでしたか?" do |q| q.answers << "良かった" q.answers << "まあまあ" q.answers << "最低" end question "次回も参加したいですか?" do |q| q.answers << "したい" q.answers << "内容によっては" q.answers << "二度と来ない" end end
コンテキスト部分の表現の改善
キーワード引数を導入し、コンテキスト部分の表現をちょっと改善。
... def context args @target = Context.new(args[:title], []) yield case args[:to] when :html puts ERB.new(TEMPLATE).result(binding) else p @target end end def question text ... end # DSL sample context :to => :html, :title => "Ruby勉強会-8のアンケート" do question "勉強会への参加は初めてですか?" do |q| q.answers << "初めて" q.answers << "常連" end question "今回の勉強会はどうでしたか?" do |q| q.answers << "良かった" q.answers << "まあまあ" q.answers << "最低" end question "次回も参加したいですか?" do |q| q.answers << "したい" q.answers << "内容によっては" q.answers << "二度と来ない" end end
アンケート部分の表現の改善
つづいて、アンケート部分の表現の改善。だいぶアンケートっぽくなりました。
... def context args ... end def question text @target.questions << Question.new(text, yield) end # DSL sample with global functions (3) context :to => :html, :title => "Ruby勉強会-8のアンケート" do question "勉強会への参加は初めてですか?" do ["初めて", "常連"] end question "今回の勉強会はどうでしたか?" do ["良かった", "まあまあ", "最低"] end question "次回も参加したいですか?" do ["したい", "内容によって", "二度とくるか"] end end
オブジェクトを使って
クラス・メソッドを使ったDSLのサンプル。
#!/user/bin/ruby -Ku require "erb" # ValueObject class Context attr_accessor :title, :questions def initialize title, questions @title = title @questions = questions end end class Question attr_accessor :title, :answers def initialize title, answers @title = title @answers = answers end end class Questionnaire TEMPLATE = <<EOP <b><%= @target.title %></b><br/> <% @target.questions.each do |q| %> <p><%= q.title %></p> <% q.answers.each do |a| %> <li><%= a %></li> <% end %> <% end %> EOP def self.context args @target = Context.new(args[:title], []) yield self case args[:to] when :html puts ERB.new(TEMPLATE).result(binding) else p @target end end def self.question text @target.questions << Question.new(text, yield) end end # DSL sample Questionnaire.context :to => :html, :title => "Ruby勉強会-8のアンケート" do |q| q.question "勉強会への参加は初めてですか?" do ["初めて", "常連"] end q.question "今回の勉強会はどうでしたか?" do ["良かった", "まあまあ", "最低"] end q.question "次回も参加したいですか?" do ["したい", "内容によって", "二度とくるか"] end end
メタプログラミングを使って
最後に、method_missingを利用したDSLのサンプル。
#!/user/bin/ruby -Ku require 'erb' TEMPLATE = <<EOP <b><%= @target.title %></b><br/> <% @target.questions.each do |q| %> <p><%= q.title %></p> <% q.answers.each do |a| %> <li><%= a %></li> <% end %> <% end %> EOP # Value Object class Context attr_accessor :title, :questions def initialize title, questions @title = title @questions = questions end end class Question attr_accessor :title, :answers def initialize title, answers @title = title @answers = answers end end def method_missing sym, *args case sym when :context @target = Context.new(args[0][:title], []) yield case args[0][:to] when :html puts ERB.new(TEMPLATE).result(binding) else p @target end when :question q = Question.new(args[0], yield) @target.questions << q end end #DSL sample context :to => :html, :title => "Ruby勉強会-8のアンケート" do question "勉強会への参加は初めてですか?" do ["初めて", "常連"] end question "今回の勉強会はどうでしたか?" do ["良かった", "まあまあ", "最低"] end question "次回も参加したいですか?" do ["したい", "内容によって", "二度とくるか"] end end