ランサーズ(Lancers)エンジニアブログ > Ruby > Rubyでデザインパターン入門

Rubyでデザインパターン入門

tomohiro|2014年08月01日
Ruby

はじめまして。ランサーズのエンジニアのtomohiroです。

先日、社内勉強会をする事になり、そのお題としてデザインパターンをやったので、内容を少し共有しようと思います。自分はデザインパターンをあんまり勉強していなかったので、この機会にデザインパターンに触れてみようと思いました。そこでお題を「(自分も)デザインパターン入門」に。ソフトウェアでデザインパターンを初めて取り上げたと言われる Gang of Four(GoF) の書籍「オブジェクト指向における再利用のためのデザインパターン」で紹介されている23種類のパターンの内から2つピックアップして、rubyで実装してみる事にしました。

そもそもデザインパターンとは?

ソフトウェアの開発で言うデザインパターンとは、開発中に頻繁に直面する問題に対する解決策をパターン化し、名前をつけた物。更に言うとノウハウを蓄積し、再利用しやすいように定義付けをしたパターンになります。単純にノウハウを貯めるだけではなくノウハウに名前が付くため、議論もしやすくなり、結果的に意思の疎通も容易になります。
ただし、デザインパターンは銀の弾ではありません。あくまでも過去の設計者達が気づいた設計パターンをまとめた物に過ぎません。そのためデザインパターンを覚えれば何でも開発出来るようになるわけでも無ければ、給料があがるわけでもありません。技術者としての知見と技術的引き出しが広がり、結果的にスピードを持って開発が出来るようになり、給料があがる事はありえます。
知らず知らずの内に誰かが定義したデザインパターンを使っている事も多々あります。逆に言語やフレームワークによっては使用が推奨されないパターンも存在します。結局、コードの書き方はある程度経験によって左右され、最終的には適材適所。引き出しを多く持っている事に越した事はないので、歴史から学び、ひとつでも多くの適材適所を増やせるようデザインパターンを知識に取り入れると良い事があるのではないでしょうか。
上で紹介した著書の名前にもあるように、オブジェクト指向に大きく関わるところもあるのですが、今回はそこには深く踏み込みません。今回は「あ、こんなコード見た事ある」って気付きを得るところから始めようかと思います。

コードを見てみましょう

早速ですが、今回ピックアップしたふたつのパターンは Template(テンプレート) と Strategy(ストラテジー) です。似ているようで異なるパターンだったので、ちょうどいいかなと思い取り上げてみました。
日本語に訳すと、
テンプレート → 型紙
ストラテジー → 戦略

パターン的にいうと
テンプレート → ある型紙をベースに処理を書き、必要に応じて処理の変更する
ストラテジー → 処理の流れを指定し、ステップ毎に内容を選択出来るようにする

よくある、車の例で説明すると、
テンプレート → 基本的な車の定義がされていて、それに変更等を加える事によって新しい物を作って、新しい車の定義を作る
(1)その複製を作る(継承)
(2)必要に応じてパーツを付け替えたり、改造したりする
ストラテジー → 車のシャーシが用意されていて、各パーツに何を当てはめるか指定していく。必要に応じてリアルタイムで変更が可能(例:タイヤを付け替える)
(1)シャーシとパーツを用意する
(2)パーツを付け替えて、必要なスペックにする

重要なポイントは、処理の定義を自ら持つのか(テンプレート)、処理流れだけを持ち、処理の定義自体は外のオブジェクトに移譲するのか(ストラテジー)だと思っています。

では実際にコードで説明してみます。
基本的にrubyを多く使ってきているので、rubyで書いています。

今回は定型文のレポートを作成するスクリプトを題材にしています。(Design Patterns in Ruby という本を読んでいたので、それにかなりインスパイアされていると思います。題材もそうだったかも ^^;)
当初はテキストレポートのみ必要だったのが、途中からHTMLレポートも必要だと言われ、レポート自動生成スクリプトを作る事にしたという想定です。

まずはかなり単純なスクリプトを書いてみました。

# -*- coding:utf-8 -*-
#
#
class Report
  def initialize(opt = {})
    @title = opt[:title]
    @data = opt[:data]
  end

  def output_report
    puts "=============================="
    puts "#{@title}"
    puts "=============================="
    puts "------------------------------"
    @data.each do |d|
      puts d
    end
    puts "------------------------------"
    puts "EOF"
  end
end

report = Report.new({
title: 'Report 2014-06',
data: ['りんご', 'ごりら', 'らっぱ', 'パイソン']
})

report.output_report

単純にputsを並べているだけですね。もしHTMLレポートを作りたければ全部書き換えればいい話しです。今あるクラスをコピペして、必要な部分を書き換える。

=========================================================

次にtemplateパターン

# -*- coding:utf-8 -*-
#
#
class Report
def initialize(opt = {})
@title = opt[:title]
@data = opt[:data]
end

def output_report
pre_document
body_start
header
table_start
table_data
table_end
body_end
post_document
end

def pre_document
# raise “abstract method: #{__method__}”
end

def body_start
# raise “abstract method: #{__method__}”
end

def header
raise “abstract method: #{__method__}”
end

def table_start
raise “abstract method: #{__method__}”
end

def table_data
raise “abstract method: #{__method__}”
end

def table_end
raise “abstract method: #{__method__}”
end

def body_end
# raise “abstract method: #{__method__}”
end

def post_document
raise “abstract method: #{__method__}”
end
end

class HtmlReport < Report def pre_document puts "" end def body_start puts "”
end

def header
puts “

#{@title}


end

def table_start
puts “


end

def table_data
@data.each do |d|
puts “


end
end

def table_end
puts “

#{d}


end

def body_end
puts “”
end

def post_document
puts “”
end
end

class TextReport < Report def header puts "==============================" puts "#{@title}" puts "==============================" end def table_start puts "------------------------------" end def table_data @data.each do |d| puts d end end def table_end puts "------------------------------" end def post_document puts "EOF" end end report = TextReport.new({ title: 'Report 2014-06', data: ['りんご', 'ごりら', 'らっぱ', 'パイソン'] })report.output_report [/code] 今回はrubyの継承 Class < SuperClass を使っています。これでReportクラスを継承し、必要な部分を上書きした上でレポート生成としてテキストレポート生成とHTMLレポート生成のクラスに分けて作りました。 ========================================================= 最後にストラテジー [code language="ruby"] # -*- coding:utf-8 -*- # # # Report # Formatter # HtmlFormatter # TextFormatter # class Report def initialize(opt = {}) @title = opt[:title] @data = opt[:data] @formatter = opt[:formatter] end def formatter(hoge) @formatter = hoge end def output_report @formatter.output_report(self) end # attr_reader: title, data def title; @title; end def data; @data; end end class Formatter def output_report(context) pre_document body_start header(context) table_start table_data(context) table_end body_end post_document end def pre_document # raise "abstract method: #{__method__}" end def body_start # raise "abstract method: #{__method__}" end def header(context) raise "abstract method: #{__method__}" end def table_start raise "abstract method: #{__method__}" end def table_data(context) raise "abstract method: #{__method__}" end def table_end raise "abstract method: #{__method__}" end def body_end # raise "abstract method: #{__method__}" end def post_document raise "abstract method: #{__method__}" end end class HtmlFormatter < Formatter def pre_document puts "”
end

def body_start
puts “”
end

def header(context)
puts “

#{context.title}


end

def table_start
puts “


end

def table_data(context)
context.data.each do |d|
puts “


end
end

def table_end
puts “

#{d}


end

def body_end
puts “”
end

def post_document
puts “”
end
end

class TextFormatter < Formatter def header(context) puts "==============================" puts "#{context.title}" puts "==============================" end def table_start puts "------------------------------" end def table_data(context) context.data.each do |d| puts d end end def table_end puts "------------------------------" end def post_document puts "EOF" end end report = Report.new({ title: 'Report 2014-06', data: ['りんご', 'ごりら', 'らっぱ', 'パイソン'], formatter: TextFormatter.new }) report.output_report report.formatter(HtmlFormatter.new) report.output_report [/code] まずはストラテジーのクラスがレポート出力の処理を行うオブジェクトを設定出来るようにしています。そして、基本的な処理の流れ(headerを書く、bodyを書く、…)を定義しています。 レポート出力処理をするオブジェクトをテキスト用とHTML用と用意して、必要に応じてその場で変更出来るようにしました。その結果、柔軟に対応出来るレポート生成スクリプトが出来ました。 いかがでしたでしょうか?意図せずに、「そういえばこんな設計した事ある!」って思った方もいたのではないでしょうか。その時は自分天才とか思っても、実際には過去にもこういう事を考えていた方がいたんですね。経験から学ぶ事は重要ですが、歴史から学ぶ事もかなり重要だと思います。もしすでにやっていなければ、改めて過去の偉人達の功績を振り返る時間を作ってみてはいかがでしょうか。ただちに影響はなくとも、将来ふとした瞬間に役に立つ日が来る事を信じて!