Rubyデザインパターン学習のために、自分なりに読書の結果をまとめていくことに決めました。第11日目はBuilderです。(http://www.amazon.co.jp/gp/product/4894712857/ref=as_li_qf_sp_asin_tl?ie=UTF8&camp=247&creative=1211&creativeASIN=4894712857&linkCode=as2&tag=morizyun00-22)
11日目 Builder
Builderは、オブジェクトを組み立てていくためのパターンです。
前回に学習したFactoryとBuilderは、オブジェクトを構築するという点で似通ったところが多いです。
大きな違いは、
- Factory:正しいオブジェクトを選択・生成する
- Builder:複雑なオブジェクトを生成・構築する
という点です。
また拡張することで、BuilderにFactoryのようなオブジェクトを選択する機能を持たせることができます。
普通のコード
まずは何も考えずに、一つずつPCのパーツを追加して構築するコードを書いてみましょう!
class Computer
attr_accessor :display
attr_accessor :motherboard
attr_accessor :drives
def initialize(display = :crt, motherboard = Motherboard.new, drives = [])
@motherboard = motherboard
@drives = drives
@display = display
end
end
class CPU
# CPU共通のコード
end
class BasicCPU < CPU
# あまり高速でないCPUに関するコード
end
class TurboCPU < CPU
# 高速なCPUに関するコード
end
class Motherboard
attr_accessor :cpu
attr_accessor :memory_size
def initialize(cpu = BasicCPU.new, memory_size = 1000)
@cpu = cpu
@memory_size = memory_size
end
end
class Drive
attr_accessor :type
attr_accessor :size
attr_accessor :writable
def initialize(type, size, writable)
@type = type
@size = size
@writable = writable
end
end
motherboard = Motherboard.new(TurboCPU.new, 4000)
drives = []
drives << Drive.new(:hard_drive, 200000, true)
drives << Drive.new(:cd, 760, true)
drives << Drive.new(:dvd, 4700, false)
computer = Computer.new(:lcd, motherboard, drives)
以上のコードが、何も考えずにPCを組み立てるコードを書いた結果です。
それぞれの部品ごとのクラスはいいのですが、それを組み立てるインターフェイスが非常に煩雑で、可読性もよろしくないので、一目で何をやっているかがわかりません!
なにかいい手はないのでしょうか?
Builderでオブジェクトを組み立てよう!
class ComputerBuilder
attr_reader :computer # builder.computerでアクセスしてcomputerオブジェクトを得るため
def initialize
@computer = Computer.new # Computerオブジェクトを作成
end
def turbo(has_turbo_cpu = true)
@computer.motherboard.cpu = TurboCPU.new
end
def display=(display)
@computer.display = display
end
def memory_size=(size_in_mb)
@computer.motherboard.memory_size = size_in_mb
end
def add_cd(writer = false)
@computer.drives << Drive.new(:cd, 760, writer)
end
def add_dvd(writer = false)
@computer.drives << Drive.new(:dvd, 4000, writer)
end
def add_hard_disk(size_in_mb)
@computer.drives << Drive.new(:hard_disk, size_in_mb, true)
end
end
builder = ComputerBuilder.new
builder.turbo
builder.add_cd(true)
builder.add_dvd
builder.add_hard_disk(100000)
computer = builder.computer # 最終的に求めていたComputerオブジェクトが得られます
GoFはBuilderオブジェクトを使うクライアントをディレクタと呼んでいます。
ディレクタは新しいオブジェクトの組み立てをビルダ(ここではComputerBuilder)に指示します。
作られたオブジェクトはプロダクト(computer)と呼ばれます。
こういった形で、**邪魔なオブジェクト生成の部分を全てビルダに分離 **することができました。
冒頭で紹介したように、オブジェクトを組み立てるだけではなく、ビルダを抽象化することによってオブジェクトを選択する機能もつけることができます。
class ComputerBuilder
attr_reader :computer
def turbo(has_turbo_cpu = true)
@computer.motherboard.cpu = TurboCPU.new
end
def memory_size=(size_in_mb)
@computer.motherboard.memory_size = size_in_mb
end
end
class DesktopBuilder < ComputerBuilder
def initialize
@computer = DesktopComputer.new
end
def display=(display)
@display = display
end
def add_cd(writer = false)
@computer.drives << Drive.new(:cd, 760, writer)
end
def add_dvd(writer = false)
@computer.drives << Drive.new(:dvd, 40000, writer)
end
def add_hard_disk(size_in_mb)
@computer.drives << Drive.new(:hard, size_in_mb, true)
end
end
class LaptopBuilder < ComputerBuilder
def initialize
@computer = LaptopComputer.new
end
def display=(display)
raise "Laptop display must be lcd" unless display == :lcd
end
def add_cd(writer = false)
@computer.drives << LaptopDrive.new(:cd, 760, writer)
end
def add_dvd(writer = false)
@computer.drives << LaptopDrive.new(:dvd, 4000, writer)
end
def add_hard_disk(size_in_mb)
@computer.drives << LaptopDrive.new(:hard_disk, size_in_mb, true)
end
end
laptop_builder = LaptopBuilder.new
laptop_builder.display = :lcd
# etc
# etc
# etc
laptop_builder.computer
オブジェクト生成を安全にする
def computer
raise "Not enough memory" if @computer.motherboard.memory_size < 250
raise "Too many drives" if @computer.drives.size > 4
hard_disk = @computer.drives.find{ |drive| drive.type == :hard_disk }
raise "No hard disk." unless hard_disk
@computer
end
最後の.computer
でコンピュータオブジェクトを作成する時点の話です。
ここで、基底クラスの.computer
をオーバーライドしておくことにより、属性チェックをしてから、それを通過した規格を満たすものだけをオブジェクトとして世に送り出すことができます。
attr_accessor
もメソッドなのでオーバーライドできることもワンポイントです
Let's Ruby !!!
これまでのコードでBuilderパターンは完成したのですが、依然として組み立てのときにたくさんのメソッドを呼ぶ必要があります。
これを解決する方法はマジックメソッドです。
それはなんだというと、Builderのクライアントが特定のパターンに従ったメソッド名を組み立て、それをビルダに適用するというものです。
def method_missing(name, *args)
words = name.to_s.split('_')
return super(name, *args) unless words.shift == 'add'
words.each do |word|
next if word == 'and'
add_cd if word == 'cd'
add_dvd if word == 'dvd'
add_hard_disk(100000) if word == 'harddisk'
turbo if word == 'turbo'
end
end
手順的には、予期せぬメソッドを必ず受け取るというのを前提にしておいて、method_missing
で補足する、というパターンです。
これによって、
builder.display
builder.motherboard
builder.drive
builder.accessories
##### so many...
のように面倒な繰り返しをしなくてよくなりますね。
rails generate migration add_post_id_to_users
Railsコマンドでも内部はこういった形の実装になっているのかもしれませんね!(詳しくは知りません!)
まとめ
Builderパターンはオブジェクトをだんだんと組み立てていくパターンです。
組み立てたいオブジェクトのクラスが巨大になるほど、Builderが威力を発揮することは多いようです。
オブジェクトを組み立てていった際に、「めんどくさいな...」と思ったら導入するメリットがあるかもしれません。