何番煎じか判りませんがお勉強メモを残します
「HeadFirstデザインパターン」第13章
「Rubyによるデザインパターン」第14章
Builder パターン
「HeadFirstデザインパターン」でのJavaコードは(だいたい)こんな感じ
第13章にて「その他のパターン」として2ページで紹介されている
- 製品の構築をカプセル化し、段階的な製品構築を可能にする
- 複数の、手順の変化するプロセスにてオブジェクトを構成できる(一度の手順によるFatoryパターンとは対照的)
- 製品の内部表現をクライアントから隠蔽する
- クライアントには抽象インターフェースしか見えない為、製品の実装を容易に交換できる
- 複合構造の構築によく使われる
- オブジェクトの構築には、Factoryパターンを使うときよりもクライアントのドメインに対する知識が必要となる
Javaでの実装コードは掲載されていない(´・ω・`)
「Rubyによるデザインパターン」でのRubyコードは(だいたい)こんな感じ
- 前章ではファクトリによる正しいオブジェクトの取得方法を見てきた
- 正しいオブジェクトを取得する事が問題なのではなく、オブジェクトを構成する時が問題になる場合がある
- 我々はデスクトップPCを製造するビジネスを支援するためのシステムを作っているとする
- それぞれのPCはオーダーメイドで、我々は各PCに詰め込まれる部品を把握する必要がある
- 話をシンプルにする為に、PCはディスプレイとマザーボード、複数のドライブから構成されている、事とする
class Computer
attr_accessor :display, :motherboard
attr_reader :drives
def initialize(display=:crt, motherboard=Motherboard.new, drives=[])
@motherboard = motherboard
@drives = drives
@display = display
end
end
- ディスプレイは :crt か :lcd のどちらかしかないです
- マザーボードはある程度のメモリがあり、並のCPUか高速CPUかどちらかを持っています
class CPU
# CPU共通のコード
end
class BasicCPU < CPU
# 並のCPUのコード
end
class TurboCPU < CPU
# 高速CPUのコード
end
class Motherboard
attr_accessor :cpu, :memory_size
def initialize(cpu=BasicCPU.new, memory_size=1000)
@cpu = cpu
@memory_size = memory_size
end
end
ドライブは3種類あります :hard_disk, :cd, :dvd
class Drive
attr_reader :type # :hard_drive, :cd, :dvd
attr_reader :size # MB
attr_reader :writable # ドライブが書き込み可能かどうか
def initialize(type, size, writable)
@type = type
@size = size
@writable = writable
end
end
↓単純なモデルにしたにもかかわらず、Computerの新しいインスタンスを組み立てるのは退屈な作業になります
# たくさんのメモリを積んだ高速なコンピュータを組み立てる
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)
Builderパターン
- Builderパターンの考え方は、この種の構築ロジックをとあるクラスにカプセル化するという単純なもの
- ビルダクラスが担当するのは、複雑なオブジェクトのコンポーネントの組み立て
- ビルダは新しいオブジェクトに対してステップごとに構成を支持するインターフェースを持っている
class ComputerBuilder
attr_reader :computer
def initialize
@computer = Computer.new
end
def turbo
@computer.motherboard.cpu = TurboCPU.new
new
def display=(display)
@computer.display = display
end
def memory_size=(memory_in_mb)
@computer.motherboard.memory_size = memory_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, 4700, writer)
end
def add_hard_disk(size_in_mb)
@computer.drives << Drive.new(:hard_drive, size_in_mb, true)
end
end
- ComputerBuilderクラスはComputerのインスタンスを作るための全ての詳細を分離します
- ビルダの新しいインスタンスを単純に作成し、コンピュータに必要なすべてのオプションを指定するプロセスを1つずつ実行する
builder = ComputerBuilder.new
builder.turbo
builder.add_cd(true)
builder.add_dvd
builder.add_hard_disk(100000)
computer = builder.computer
- Gofはビルダオブジェクトのクライアントをディレクタ(director)と呼んでいる ビルダに指示するものである
- 作られるオブジェクトは製品(Product)と呼ばれる
- ビルダは複雑なオブジェクトを作る負荷を軽減するだけでなく、その実装の詳細を隠蔽するする役割も持っている
- ディレクタは新しいオブジェクトを作るための詳細を知る必要は無い
- ComputerBuilderクラスを使うことで、DVDやハードディスクを表すクラスがどれなのか知る必要は無いという訳である
- Builderパターンは正しいクラスを見つけることに関心をおいているのではなく、オブジェクトを構成することに焦点をおいている
- 邪魔なオブジェクト生成のコードを分離するのがビルダの主な目的
- ビルダをオブジェク生成にすれば、「クラスの選択」をする際にも便利に使うことが出来る
PC製造ビジネスの続き
- ラップトップPC (ノートPC) も扱うようになります
- つまり デスクトップPC と ラップトップPC、2種類の商品を扱うようになります
class DesctopComputer < Computer
# デスクトップPCに関するコード
end
class LaptopComputer < Computer
def initialize(motherboard=Motherboard.new, drives=[])
super(:lcd, motherboard, drives)
end
# デスクトップPCに関するコード
end
- ラップトップPCの部品はデスクトップPCのそれとは同じではない
- ビルダの基底クラスを作り、2つのサブクラスでそれらの違いを扱うようにリファクタリングすることができる
- この抽象的なビルダの基底クラスで、2種類のコンピュータの共通部分を扱う
class ComputerBuilder
attr_reader :computer
def turbo
@computer.motherboard.cpu = TurboCPU.new
new
def memory_size=(memory_in_mb)
@computer.motherboard.memory_size = memory_in_mb
end
end
class DesctopBuilder < 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, 4700, writer)
end
def add_hard_drive(size_in_mb)
@computer.drives << Drive.new(:hard_drive, size_in_mb, true)
end
end
# ラップトップPCのドライブはデスクトップPCとは違うので LaptopDrive.new を使うようになっています
class LaptopBuilder < ComputerBuilder
def initlialize
@computer = LaptopComputer.new
end
def display=(display)
raise "LCD only" if 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, 4700, writer)
end
def add_hard_drive(size_in_mb)
@computer.drives << LaptopDrive.new(:hard_drive, size_in_mb, true)
end
end
- ビルダはオブジェクトの生成を簡単にするだけでなく、より安全にすることができる
- ビルダの最後に実行する「できあがったオブジェクトを下さい」メソッドはクライアントから要求された構成が、ルールに則っているかチェックするための理想的な場所になる
class ComputerBuilder
def computer
raise 'メモリなし' if @computer.motherboard.memory_size < 250
raise 'ドライブ多杉' if @computer.drives.size > 4
raise 'HDなし' if drives.find { |d| d.type == :hard_drive }.nil?
@computer
end
end
まとめ
- Builderパターンの必要性はアプリケーションが複雑になるにつれて大きくなる
- 例えば、最初のComputerクラスはCPUの型やメモリの大きさを把握できればよかっただけかもしれません。そのような単純なクラスへのビルダの適用は明らかにやりすぎ
- しかし、Computerクラスを拡張し、ドライブやオプションをモデル化し、それらのオプション間の依存関係が爆発するようならビルダはより意味を持つ
- Factoryパターンの時と同様に、Builderパターンを誤用してしまうよくあるケースは「必要ないのに使ってしまう」事
- ビルダを使うのは蔵祭する要求が手に負えなくなった時である