Posted at

Rubyデザインパターン 11日目 : Builder

More than 3 years have passed since last update.

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)

スクリーンショット 2015-07-27 11.25.28.png


 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が威力を発揮することは多いようです。

オブジェクトを組み立てていった際に、「めんどくさいな...」と思ったら導入するメリットがあるかもしれません。