大規模開発でも使えるRailsテクニック(1) 変更に強いメソッドを組む

 久々にブログを書きます。

 普段は大手SIerRuby開発をしているKirikaです。私の会社では4年ほど前からRubyをシステム開発に活用していくように取り組んでいます。当初はお客様、自社の人間を含めてRubyに向けられる視線は非常に懐疑的でありましたが、少しずつ実績を重ね、最近になって、ようやく大規模プロジェクトにも適用出来るという評価が得られるようになってきたと実感しております。

 私は現在Railsフレームワークを拡張して、独自の業務フレームワークを載せる仕事をしておりますが、私が2年間関わってきた大規模と呼べるプロジェクトの中で、Ruby / Railsのこういうところは大規模開発にも使えるよね、と思った点を更新していこうと思います。このブログを参照する方の中には、当たり前のようなことしか書かれていないと思われる方もいらっしゃいますでしょうが、ご容赦ください。

変更に強いメソッドを組む

 フレームワークの仕事でまず重要なのは、開発者にどのようなメソッドを提供するかを決定するところです。ここが決まれば、仮に実装がまだ出来ていなくて空の状態でも開発を進めることができます。開発者がどのように呼び出せば開発がしやすいかを考えるところが重要なのですが、私は呼び出しの箇所をとりあえず以下のように定義しておきます。

def convert(target, options = {}, &block)
  ...
end

 この書き方はRailsの中ではよく見かける書き方なので、知っている人にはあまり目新しい話ではないと思いますが、target引数には処理対象、optionsにはHashを定義して、パラメータの数を自由に増減できるように定義します。私の経験上、メソッドの呼び出しで必須パラメータとなるのは多くても3個程度で、それ以上になると呼び出し側でも煩雑になり、コードの可読性が落ちます。なるべく必須パラメータを減らしましょう。

 どこの開発でもありがちだとは思いますが、大抵最初に取り決めた要件では仕様を満たすことができなくなって、メソッドを拡張していくことになりますが、この形を取っていればoptionsにパラメータを1つ追加するだけで良いので、外部のインターフェースを変更する必要がなく、メソッドの実装をコピペして、別のメソッドを定義する必要がなくなりますw(自分の身内でそんなことをする人がいたら鉄拳制裁ですが…。)

 &blockは必ずしも必須というわけではありませんが、引数に開発者が独自にロジックを定義したいというケースはよくあります。Rubyにはlambdaがあるので、optionsの引数の一つとしてlambda式を指定する、という書き方でも良いのですが、可読性を考えるとブロックに指定したほうが見栄えは良いです。ただし1個しか使えないので、大切に使う必要があります。

 &blockを渡すメソッドを書くときは内部でblock.call、もしくはblock.yieldで呼び出します。callはブロック引数の個数をチェックする呼び出し、yieldはブロック引数の個数をチェックしない呼び出しです。開発者がブロックを定義する際にブロック引数の定義を固定したいのであればcall、そうでなければyieldを使うのが良いです。

def convert(target, options = {}, &block)
  ...
  block.call if block_given? # &もしくはblock.yield
end  

 他にlambdaを使ったテクニックとしては、パラメータの一要素の型で処理を切り替える、というものがあります。

 例えば、optionsに:messageという引数が使えたとして、当初は静的な文字列を出力するだけで良かったのに、開発が進んでくるとパラメータの値によって動的に値を返さなければならない、というようなケースはよくあると思います。

 例としてconvertメソッドというものがあって、messageオプションには処理が完了した時に出力するメッセージを定義できる、という仕様だったとして、開発者は既に以下のように呼び出している場合を考えてみましょう。

convert :title, :message => "converted!"

 この時、ある機能の呼び出しでは、動的に変更前、変更後のメッセージを動的に埋め込まなければならない、という事が決まった(もしくは発覚した)ということがあった場合のケースを考えてみます。Javaならオーバーロードが使えるので、同じメソッドを定義して解決すると思います。(そして、オーバーロードしたメソッドは大抵実装元がコピペされています。)

 Rubyはオーバーロードがありませんが、メソッドの中で引数の型を判定することで、オーバーロードと同じようなことができます。実装例としては以下のようなものです。

def convert(target, options = {}, &block)
original = self.__send__(target)
converted_string = ... # 例えばここで変換処理
case options[:message].class
when String, Symbol
p options[:message]
when Proc
p options[:message].yield(original, converted_string)
end end

 こうすることで、以下のようにどちらの書き方でも、メソッドは処理してくれるようになります。

convert :title, :message => "converted!"
convert :title, :message => lambda{|before, after|
 "#{before} convert to #{after}" }

 この辺りの書き方もRailsのソースを読んでいたらよく出てくる書き方なので、知っている人は多いかもしれませんね。

 このような実装をするときは、既存のソースコードをどんどん書き換えて行くので、RSpec等を使ってテストドリブンにした方が効率がいいです。