Rubyでのデザインパターン
デザインパターンといえば、こちらの本が有名で大変ためになる本ですが、これらのデザインパターンを ruby で書こうとするとどうなるのかというのでこちら購入して読んでみました。
Design Patterns in Ruby (Addison-Wesley Professional Ruby Series) (英語)
はい、英語です。日本語版もあるのですが、6000 円ほどで結構高かったり英語の技術書を読破するという今年の目標もあったので、こちらを選びました。
TemplateMethodパターン
早速デザインパターンの紹介にうつります。第一回目は TemplateMethod パターンです。
class Report
def initialize
@title = 'Monthly Report'
@text = ['THings are going', 'really, really well']
end
def output_report
output_start
output_head
output_body_start
output_body
output_body_end
oputput_end
end
def output_body
@text.each do |line|
output_line(line)
end
end
def output_start
end
def output_head
output_line(@title)
end
def output_line(line)
raise 'Called abstract method: output_line'
end
def output_body_end
end
def output_end
end
end
先にコードをのせてしまいましたが、Template Method パターンではこのような抽象クラスを定義して、この Report クラスのようなクラスを継承した具象クラスでメソッドをオーバーライドして実装していきます。
このデザインパターンのミソはこのコードでいうとouput_reportメソッドになります。このメソッド内で実行する処理の順番などを定義して、そこに定義された分割されたメソッドの実際の挙動はサブクラスで定義していくというような実装になります。
サブクラスでは、サブクラスでそのサブクラス特有の処理だけ実装すればよくなる(output_report のようなメソッドは共通処理なので抽象クラスに実装しておくだけですむ)ので、全体としてコードの重複をさけて記述量を減らすことができます。
Template Methodで適用する例
先ほど紹介したデザインパターンの例を引用しますが、ある日月次のレポートとして HTML でのレポートを作成する必要がありそれを実装することになった場合を考えます。
class Report
def initialize
title = 'Monthly Report'
@text = [ 'Things are going', 'really, really well.' ]
end
def output_report
puts('<html>')
puts(' <heade>')
puts(' <title>#{@title}</title>')
puts(' </head>')
puts(' <body>')
@text.eah do |line|
puts(' <p>#{line}</p>')
end
puts(' </body>')
puts('</html>')
end
end
HTML を出力するだけなので、このよな実装をしたとします。しかし、後からテキスト形式でも出してほしいといったときにこまってしまいます。
ここで付け焼き刃的な実装をするとこんな感じになってしまいます。
class Report
def initialize
@title = 'Monthly Report'
@text = ['Things are going', 'really, really well']
end
def output_report(format)
if format == :plain
puts('*** #{@title{} ***')
elsif format == :html
puts('<html>')
puts(' <heade>')
puts(' <title>#{@title}</title>')
puts(' </head>')
puts(' <body>')
else
raise "Unknown format: #{format}"
end
@text.eah do |line|
if format == :plain
puts(' <p>#{line}</p>')
else
puts(' <p>#{line}</p>')
end
end
puts(' </body>')
puts('</html>')
end
end
うーん、動きはしそうですが、if 文が多くてなかなか読みづらいですね。しかも、こんどは別の csv 形式で出力してほしいとなった場合にまた if 文が増えて辛くなりそうな気しかしないです。
こういうときに Template Method パターンで実装をすると、一番最初に紹介した Report クラスを継承する形で HTMLReport クラスと TextReport クラスを実装するとだいぶ楽になります。
HTMLReport.rb
class HTMLReport < Report
def output_start
puts('<html>')
end
def output_head
puts(' <head>')
puts(" <title>#{@title}</title>")
puts(' </head>')
end
def output_body_start
puts('<body>')
end
def output_line(line)
puts(" <p>#{line}</p>")
end
def output_body_end
puts('</body>')
end
def output_end
puts('</html>')
end
end
TextReport
class TextReport < Report
def output_head
puts("*** #{@title} ***")
puts
end
def output_line(line)
puts(line)
end
end
このようにそれぞれの出力フォーマットを Report クラスのサブクラスとして切り出すことによってすっきりとしたコードになりました。
これらのクラスを使う場合は、出力したいフォーマットにあわせて
# HTML出力する場合
HTMLReport.new.output_report
# Text出力する場合
TextReport.new.output_report
まとめ
ここまでで、TemplateMethod パターンの紹介は以上です。デザインパターンはなかなか覚えるのに時間かかかるのですが、実装前の段階でデザインパターンを知っているか知らないかで実装の簡潔さや変更の容易さがかわってきて知っていると本当に便利です。
最初にもちょっと触れましたが、TemplateMethod パターンの肝は抽象クラスのそれぞれのメソッドを呼び出す親メソッド(Report クラスの output_report メソッド)とサブクラスでの実装を強制するメソッド です。
親メソッドというかテンプレートメソッドがどういう順番で個別のメソッドを呼び出すかというのを定義して、それに合わせてサブクラスでメソッドをオーバライドしていくので当然このテンプレートメソッドがクラス設計の重要な部分になってきます。設計時にはどういうパターンが今後の変更などを加味してどういうメソッドにするのかというのを決めていきましょう。
また、Report クラスでは output_line ではメソッド内で例外をなげるように実装することで子クラスでオーバーライドされない場合にはエラーで処理を止めるように実装しています。一方その他のメソッドはデフォルトで返却する値(もしくは何もしない)を決めているのでサブクラスで固有の処理がない場合はそのまま Report クラスのメソッドを実行するようにして、コードの重複を減らしています。
この大きくわけて二つのテンプレートメソッドと、オーバライド必須のメソッドを覚えておけばわりとするっと実装できるのが TemplateMethod パターンです。
ぜひ実務や個人開発でも TemplateMethod パターンが適用できないかどうか考えて実装してきれいなコード書いていきましょう。