Rubyで動的にメソッドを呼び出したいときの試行錯誤メモ
最近、昔のドラゴンボール劇場版を見て、90年代を懐かしんでる@masudaKです。 今回は、Rubyで条件分岐からファクトリ作成に至るまでの試行錯誤をちょいと晒してみたいと思います。 思考の流れはこんな感じ。
- ある文字列が複数個リストに入ってる。
- その文字列を特定の条件によってふるいにかけて、特定の処理をさせたい
- 今のところ行う処理は一つでいいけど、そのうち増えそう(想定はしてる)
という感じです。
リストはこんな感じ。
["roles/test/test001.json", "nodes/test/test002.json", "Rakefile"]
んで、このリストをeachで回して、roles, nodes, cookbooksみたいな文字列にマッチしたら、それぞれ違う処理をさせたいわけです。 rolesにマッチしたらroles用の処理、nodesにマッチしたらnodes用の処理みたいな。
ってことで、ちょいと勉強ついでに備忘録として残しておきます。書いてるコードはRuby1.9.3ベースですが、 基本的な部分はRubyの違うバージョンであっても、違う言語であっても、LLであればそんな大きくは変わらない気がします。 Enumerableを扱ってる部分を考えると、Rubyのほうがいいかもぐらいの温度感で見てもらえればと。 間違い等あれば、@masudaKまでご指摘お願いします。
まずシンプルに
ということで、書いてみましょう。すごーくシンプルに書くなら、以下のような感じかと。
#!/user/bin/env ruby # -*- coding: utf-8 -*- require 'open3' def roles_executer(_file_path) raise TypeError, "_file_path" unless _file_path.is_a?(String) stdout, stderr, status = Open3.capture3("echo 'this is roles method: #{_file_path}'") p stdout p stderr end def nodes_executer(_file_path) raise TypeError, "_file_path" unless _file_path.is_a?(String) stdout, stderr, status = Open3.capture3("echo 'this is nodes method: #{_file_path}'") p stdout p stderr end def cookbooks_executer(_file_path) raise TypeError, "_file_path" unless _file_path.is_a?(String) stdout, stderr, status = Open3.capture3("echo 'this is cookbooks method: #{_file_path}'") p stdout p stderr end if __FILE__ == $0 # このextract_listが処理する対象のリスト extract_list.each { |value| case value when /roles/ then roles_executer(value) when /nodes/ then nodes_executer(value) when /cookbooks/ then cookbooks_executer(value) end } end
以下がメインの処理ですね。
if __FILE__ == $0
extract_list.each { |value|
case value
when /roles/ then
roles_executer(value)
valueに対して正規表現でマッチさせて、〜executer()を呼び出す。すごくシンプルです。 んで、〜executer()は同じ引数を受け取とるようにしておく。そうすることで、〜executer()は同じようなメソッドだと伝える。 ただ、ふと思ったのが、〜executer()を管理する場所が欲しくなったわけです。~executer()ってものがどれくらいあるか、さくっと伝えられたらいいなと。
メソッドをまとめてみる
ということで、まとめてみました。
EXECUTERS = { 'roles' => :roles_executer, 'nodes' => :nodes_executer, 'cookbooks' => :cookbooks_executer } if __FILE__ == $0 extract_list.each { |value| case value when /roles/ then __send__ EXECUTERS['roles'], value when /nodes/ then __send__ EXECUTERS['nodes'], value when /cookbooks/ then __send__ EXECUTERS['cookbooks'], value end } end
ここではハッシュで定義されたシンボルを使って、実行するメソッドを定義してます。んで、それをsendで呼び出すことで、各メソッドが呼ばれるようにしています。ハッシュを使うことで、そのハッシュを経由して、メソッドを呼び出すようにしてみました。 また、whenのブロックは一行にまとめられるので、以下のようにしてしまいましょう。
when /roles/; __send__ EXECUTERS['roles'], value
when /nodes/; __send__ EXECUTERS['nodes'], value
when /cookbooks/; __send__ EXECUTERS['cookbooks'], value
end
メリットとしてはハッシュを見ることで、どのようなものが呼び出されてるかわかりやすいということでしょうか。そして、〜executer()という名前になっていれば、ダックタイピング的な処理をしてるんだろうと想像できる。そういう意味で、開発者の意図が伝わりやすいのかなとも思ってます。 その一方で、条件を増やしたくなった場合は、EXECUERSを修正する必要が出てくるので、ただ条件を増やしたいだけなのに、EXECUTERSまで手を加えることになってしまいます。その辺は微妙かなと思ったり。 ということで、まとめつつ他に影響与えないようなことができないかと思ったときに使えそうなのがファクトリメソッドパターンです。 要は、正しいオブジェクトを生成するメソッドの選択をちゃんとしてくれるよう作り、オブジェクトの生成部分もしっかりまとめておけば、管理も利用も生成も楽になるかなと。ということで、実装を変えてみましょう。
ファクトリメソッドパターン
ファクトリメソッドパターンというのは以下のように書かれています。
Factory MethodパターンはTemplate Methodパターンをオブジェクトの生成に応用したものです。ルーツであるTemplate Methodと同様、このパターンも「クラスの選択」という問いをサブクラスに答えさせます。 『Rubyによるデザインパターン』
Factory Method パターンは、他のクラスのコンストラクタをサブクラスで上書き可能な自分のメソッドに置き換えることで、 アプリケーションに特化したオブジェクトの生成をサブクラスに追い出し、クラスの再利用性を高めることを目的とする。 Factory Method パターン
なので、まずオブジェクトを生成するクラスが必要となります。ということで、オブジェクトを生成するためのクラス、Roles, Nodes, Cookbooksを作成します。
class Path protected def execute() raise 'error' end end class Roles < Path public def execute() stdout, stderr, status = Open3.capture3("echo 'this is roles method'") p stdout p stderr end end class Nodes < Path public def execute() stdout, stderr, status = Open3.capture3("echo 'this is nodes method'") p stdout p stderr end end class Cookbooks < Path public def execute() stdout, stderr, status = Open3.capture3("echo 'this is cookbooks method'") p stdout p stderr end end if __FILE__ == $0 extract_list.each { |value| case value when /roles/; obj = Roles.new; obj.execute if obj when /nodes/; obj = Nodes.new; obj.execute if obj when /cookbooks/; obj = Cookbooks.new; obj.execute if obj end end
Pathという抽象クラス的なものを作り、それを継承する各Roles、Nodes、Cookbooksクラスを作ります。 んで、それぞれのクラスにexecuterメソッドを定義する。なのでメインメソッド的な箇所では、必要なオブジェクトを生成するだけです。
ただ、これだとメインメソッド部分に、caseによる分岐が入っていて、何か追加したくなったら、ここを書きなおさないといけない。 ので、次のようにオブジェクトを生成する「工場」を別に用意しましょう。そして、この工場に引数で値を渡すことで、正しいものを作ってもらうことにしましょう。
def file_factory(_file_path) raise TypeError, "_file_path" unless _file_path.is_a?(String) case _file_path when /roles/; obj = Roles.new when /nodes/; obj = Nodes.new when /cookbooks/; obj = Cookbooks.new end obj end if __FILE__ == $0 extract_list.collect { |value| file_factory(value) }.each{ |executer| executer.execute if executer } end
こうするとfile_factoryメソッドにどのメソッドを呼ぶかを管理させることができます。mainメソッド的な場所では、工場を呼び出して、ちゃんと引数に値を渡しておけば、正しいオブジェクトが返ってくると信じる。んで、あとはそれをexecuteするだけです。
このようにしておくと、cookbooks, roles, nodesに共通の機能を加えつつ、各自それぞれの実装をすることも可能となるかと。 問題は、Rubyデザパタ本にも書かれていますが、無駄に余計な実装をしてしまうことかもしれません。
13.9 Factoryパターンの使用上の注意:
本章で調べてきたどのオブジェクト生成のテクニックにしても、失敗する最悪の方法は使うべきではないところに使ってしまうことです。すべてのオブジェクトをファクトリから生成する必要があるわけではありません。大抵の場合は、ほとんどのオブジェクトを、単にMyClass.newを呼ぶことで生成したいと思うでしょう。本章で議論したテクニックは、複数の異なる関連したクラスがあり、その中から選ばなければならない場合にのみ使用してください。 You Aint't Gonna Need It(必要になるまで作るな)を思い出してください。YAGNI原則はファクトリに対しても見事に当てはまります。ある時はアヒルとスイレンしか扱っていませんでしたが、将来トラと樹木にも対応しなければならないかもしれません。それに備えて今ファクトリを作るべきでしょうか。おそらく違います。現在は使われていないファクトリの基盤を追加するコストと将来実際にファクトリを必要とするであろう見込みとのバランスをとらなくてはいけません。あとでファクトリに合わせるためのコストの要因となります。答えを出すためには対象については詳しく知る必要はありますが、一般にエンジニアはカヌーで十分なところにクイーンメリー号(またはタイタニック号?)を作る傾向があります。もし今はクラスの選択肢が1つしかないのなら、ファクトリを作るのは先送りしてください。 『Rubyによるデザインパターン』
終わりに
今回の例で言えば、メインメソッド部分で条件分岐があるにしても、それが大きく変化することはないだろうし、各分岐でやりたいことが増えれば、 新しいメソッドを追加することで(たとえば、nodes_delete)、一番最初に示したcase文だけの処理でも工夫次第ではうまくいくでしょう。
ただ、リストにある要素がnodesにマッチし、かつファイルが存在する場合は処理1、ファイルが存在しない場合は処理2とかの規模になってくると、 ネストが深くなってくるので、classを使って、責務を分けておいたほうが楽かもしれません。その辺は最初からどこまで設計するかに拠るのかなと。 個人的にはクラスに責務持たせたいのと、メインメソッド的な部分であまりゴチャゴチャしたことしたくなかったので、工場を通して、処理する一番最後の方法をとっています。ただ、クイーンメリー号を作ってるんではないかという疑念も頭の片隅から離れないのは事実です。
と、ここまで書いたことがどこまで合ってるか分かりませんが、自分の頭を整理する意味で書いてみました。 ここはこうしたほうがいいとか、これだとこのときに困るとか、後学のためにもご指摘頂けると嬉しく思います。