
DP.03:オブジェクトの生成ステップを意識した書き方をする。- Builder -【Python】
【1】Builderパターン概要
Builderパターンもオブジェクトを生成するときの書き方のバリエーションの1つ。
(Abstract Factoryとよく似ているが、)特徴は「複数のオブジェクトの生成ステップがわかりやすくなるような書き方をする」、「複数のオブジェクト生成ステップを経て1つのオブジェクトを返す挙動にする」といった点がある。
【2】例1:PC購入時のスペックカスタマイズ
例として、PC購入時にスペックカスタマイズをしてほしいスペックを持ったPCを生成するプログラムを考えてみる。
■サンプルとするクラスオブジェクト:Computerクラス
class Computer:
def __init__(self, serial_number):
self.serial = serial_number
self.memory = None
self.ssd = None
self.gpu = None
def __str__(self):
info = (f'Memory: {self.memory}GB',
f'SSD: {self.ssd}GB',
f'Graphics Card: {self.gpu}')
return '\n'.join(info)
このComputerオブジェクトの「memory」「hdd」「gpu」の値をカスタマイズして、最終的に値が入ったComputerオブジェクトを生成する。
なお、「__str__()」については以下参照。簡単に言うと「print()」をコールした時に表示させるものを定義している。
【3】builderとdirectorという考え方
Builderパターンでは「The builder」と「The director」という特定の役割を持ったオブジェクトを用意する。
・The builder:対象オブジェクトに対して値を設定する役割を担う
・The director:builderオブジェクト行う値の設定順を制御する役割を担う
■The builder(対象オブジェクトに対して値を設定する)部分の例
# 対象とするオブジェクト
class Computer:
def __init__(self, serial_number):
self.serial = serial_number
self.memory = None
self.ssd = None
self.gpu = None
def __str__(self):
info = (f'Memory: {self.memory}GB',
f'SSD: {self.ssd}GB',
f'Graphics Card: {self.gpu}')
return '\n'.join(info)
# ComputerBuilderクラス:対象オブジェクトに値をセットするクラス
class ComputerBuilder:
def __init__(self):
self.computer = Computer('AZ12345678') # Computerオブジェクトをもたせるシリアルは今回は適当に設定
# 以下パーツのパラメータ設定
def configure_memory(self, amount):
self.computer.memory = amount
def configure_ssd(self, amount):
self.computer.ssd = amount
def configure_gpu(self, gpu_model):
self.computer.gpu = gpu_model
この「対象オブジェクト」と「The builder」を使って、1ステップ1ステップ対象オブジェクトに値を設定していく操作をコールする役割が「The director」。
■The director(builderオブジェクト行う値の設定順を制御する)部分の例
# director相当のインスタンス:builderクラスを使って対象オブジェクトに値を設定
class HardwareEngineer:
def __init__(self):
self.builder = None # 使用するbuilderを保持する
def construct_computer(self, memory, ssd, gpu):
self.builder = ComputerBuilder() #使用するbuilderを指定
steps = (self.builder.configure_memory(memory),
self.builder.configure_ssd(ssd),
self.builder.configure_gpu(gpu))
[step for step in steps]
# 以下でも同じで結果はあるが、1ステップずつ処理を踏んでいく、
# という意味合いで上記のような書き方をしている。
# self.builder.configure_memory(memory)
# self.builder.configure_ssd(ssd)
# self.builder.configure_gpu(gpu)
#propertyデコレータ
@property
def computer(self):
return self.builder.computer
# 動作確認
def main():
engineer = HardwareEngineer() # directorオブジェクト生成
engineer.construct_computer(ssd=512,memory=32,gpu='GTX3080') # directorに値を渡してオブジェクトに値を設定させる
# directorからComputerオブジェクトを受け取る
computer = engineer.computer
... 以下 受け取ったComputerオブジェクトを使って処理がつづく ...
【4】全体コード
# Computer クラス:対象とするオブジェクト
class Computer:
def __init__(self, serial_number):
self.serial = serial_number
self.memory = None
self.ssd = None
self.gpu = None
def __str__(self):
info = (f'Memory: {self.memory}GB',
f'SSD: {self.ssd}GB',
f'Graphics Card: {self.gpu}')
return '\n'.join(info)
# ComputerBuilderクラス:対象オブジェクトに値をセットするクラス
class ComputerBuilder:
def __init__(self):
self.computer = Computer('AZ12345678')# Computerオブジェクトをもたせるシリアルは今回は適当に設定
# 以下パーツのパラメータ設定
def configure_memory(self, amount):
self.computer.memory = amount
def configure_ssd(self, amount):
self.computer.ssd = amount
def configure_gpu(self, gpu_model):
self.computer.gpu = gpu_model
# directorインスタンス:builderクラスをつかって値の入ったComputerオブジェクトを作る
class HardwareEngineer:
def __init__(self):
self.builder = None
def construct_computer(self, memory, ssd, gpu):
self.builder = ComputerBuilder() #使用するbuilderを指定
steps = (self.builder.configure_memory(memory),
self.builder.configure_ssd(ssd),
self.builder.configure_gpu(gpu))
[step for step in steps]
# 以下でも同じで結果はあるが、1ステップずつ処理を踏んでいく、
# という意味合いで上記のような書き方をしている。
# self.builder.configure_memory(memory)
# self.builder.configure_ssd(ssd)
# self.builder.configure_gpu(gpu)
#propertyデコレータ
@property
def computer(self):
return self.builder.computer
def main():
engineer = HardwareEngineer() # directorオブジェクト生成
engineer.construct_computer(ssd=512,memory=32,gpu='GTX3080') # directorに値を渡してオブジェクトに値を設定させる
# directorからComputerオブジェクトを受け取る
computer = engineer.computer
print(computer)
if __name__ == '__main__':
main()
#実行結果
Memory: 32GB
SSD: 512GB
Graphics Card: GTX3080
今回は「The director」内のstep部分はあまり順序が関係なかった。
class HardwareEngineer:
... ...
def construct_computer(self, memory, ssd, gpu):
self.builder = ComputerBuilder() #使用するbuilderを指定
steps = (self.builder.configure_memory(memory),
self.builder.configure_ssd(ssd),
self.builder.configure_gpu(gpu))
[step for step in steps]
... ...
作成するプログラムによってはこのstep部分の順序が重要となることもある。
例えば、冷凍ピザなどのような冷凍加工食品では、加工順を間違えると商品はできあがらない。(食品加工装置のベルトコンベア上にピザ生地がない状態で、ソースを塗る処理やトッピング処理してもピザは完成しない)
【5】例2:ピザをつくる食品加工装置
オブジェクト生成における実行順(step部分)を気にする例として、「ピザ・マルゲリータ」をつくる食品加工装置をプログラムする。
■サンプルとするクラスオブジェクト:Pizzaクラス
from enum import Enum
# 疑似的な設定値('項目','設定値 設定値 ... ...') # 各設定値はスペース空け
# 今回はEnumを使用したが、本来はDBなど外部データにする
PizzaProgress = Enum('PizzaProgress', 'queued preparation baking ready')
PizzaDough = Enum('PizzaDough', 'hand_tossed pan-pizza crispy')
PizzaSauce = Enum('PizzaSauce', 'tomato special_mayonnaise creme_fraiche')
PizzaTopping = Enum('PizzaTopping', 'gouda mozzarella bacon ham mushrooms mentaiko red_onion oregano shrimp')
#### 対象オブジェクト
# ピザクラス
class Pizza:
def __init__(self, name):
self.name = name
self.dough = None
self.sauce = None
self.topping = []
def __str__(self):
return f'{self.name}, {self.dough}, {self.sauce}, {self.topping}'
def prepare_dough(self, dough):
self.dough = dough # Enumオブジェクトのいずれかが設定される
print(f'preparing the {self.dough.name} dough of your {self}')
このPizzaオブジェクトに対して、
・The builder経由で値を設定していく
・The directorを使ってThe builderの動作順を制御する
ということを行う。
※enumをつかっているが、詳細は以下参照。
■The builder(対象オブジェクトに対して値を設定する)部分の例
#### The Builder
# Margarita Builder
class MargaritaBuilder:
def __init__(self):
# builder内部でPizzaオブジェクトをメンバ変数(プロパティ)として保持させる
# self.pizza.●●で保持したPizzaオブジェクトへの処理となる
self.pizza = Pizza('margarita')
self.progress = PizzaProgress.queued # 工程ステータスの初期値はqueued
self.baking_time = 3 # 焼き時間
# 以下マルゲリータを作るときの各種ステップを用意する
def prepare_dough(self):
self.progress = PizzaProgress.preparation
self.pizza.prepare_dough(PizzaDough.crispy)
def add_sauce(self):
print('adding tomato sauce...')
self.pizza.sauce = PizzaSauce.tomato
print('....done adding sauce')
def add_topping(self):
topping_items = (PizzaTopping.mozzarella, PizzaTopping.oregano)
print('adding topping...')
self.pizza.topping.append([t for t in topping_items])
print('....done topping')
def bake(self):
self.progress = PizzaProgress.baking
print('baking....')
time.sleep(self.baking_time)
self.progress = PizzaProgress.ready
print('....baked!')
▲builder側に使用するstepを用意しておく
■The director(builderオブジェクト行う値の設定順を制御する)部分の例
###### The director相当
class PizzaOperator:
def __init__(self):
self.builder = None #使用するbuilderの初期値はNone
def make_pizza(self,builder):
#使用するbuilderを設定
self.builder = builder
# stepsとして実行する関数オブジェクトをつんでおく
steps = (builder.prepare_dough,
builder.add_sauce,
builder.add_topping,
builder.bake)
# 指定した関数オブジェクトを順番に実行する
[step() for step in steps]
@property
def pizza(self):
return self.builder.pizza # builderでつくられたPizzaオブジェクトを返す
## 動作確認部分
def main():
builder = MargaritaBuilder() # builder生成
pizza_operator = PizzaOperator() # the directorを生成
pizza_operator.make_pizza(builder) # the directorにthe builderを作成してpizzaを生成する
my_pizza = pizza_operator.pizza
print(f'your pizza : {my_pizza}')
【6】全体コード2
from enum import Enum
import time
# 疑似的な設定値('項目','設定値 設定値 ... ...') # 各設定値はスペース空け
# 今回はEnum使用。
PizzaProgress = Enum('PizzaProgress', 'queued preparation baking ready')
PizzaDough = Enum('PizzaDough', 'hand_tossed pan-pizza crispy')
PizzaSauce = Enum('PizzaSauce', 'tomato special_mayonnaise creme_fraiche')
PizzaTopping = Enum('PizzaTopping', 'gouda mozzarella bacon ham mushrooms mentaiko red_onion oregano shrimp')
#### 対象オブジェクト
# ピザクラス
class Pizza:
def __init__(self, name):
self.name = name
self.dough = None
self.sauce = None
self.topping = []
def __str__(self):
return f'{self.name}, {self.dough}, {self.sauce}, {self.topping}'
def prepare_dough(self, dough):
self.dough = dough # Enumオブジェクトのいずれかが設定される
print(f'preparing the {self.dough.name} dough of your {self}')
#### The Builder
# Margarita Builder
class MargaritaBuilder:
def __init__(self):
# builder内部でPizzaオブジェクトをメンバ変数(プロパティ)として保持させる
# self.pizza.●●で保持したPizzaオブジェクトへの処理となる
self.pizza = Pizza('margarita')
self.progress = PizzaProgress.queued # 工程ステータスの初期値はqueued
self.baking_time = 3 # 焼き時間
# 以下マルゲリータを作るときの各種ステップを用意する
def prepare_dough(self):
self.progress = PizzaProgress.preparation
self.pizza.prepare_dough(PizzaDough.crispy)
def add_sauce(self):
print('adding tomato sauce...')
self.pizza.sauce = PizzaSauce.tomato
print('....done adding sauce')
def add_topping(self):
topping_items = (PizzaTopping.mozzarella, PizzaTopping.oregano)
print('adding topping...')
self.pizza.topping.append([t for t in topping_items])
print('....done topping')
def bake(self):
self.progress = PizzaProgress.baking
print('baking....')
time.sleep(self.baking_time)
self.progress = PizzaProgress.ready
print('....baked!')
###### The director相当
class PizzaOperator:
def __init__(self):
self.builder = None #使用するbuilderの初期値はNone
def make_pizza(self,builder):
#使用するbuilderを設定
self.builder = builder
# stepsとして実行する関数オブジェクトをつんでおく
steps = (builder.prepare_dough,
builder.add_sauce,
builder.add_topping,
builder.bake)
# 指定した関数オブジェクトを順番に実行する
[step() for step in steps]
@property
def pizza(self):
return self.builder.pizza # builderでつくられたPizzaオブジェクトを返す
def main():
builder = MargaritaBuilder() # builder生成
pizza_operator = PizzaOperator()
pizza_operator.make_pizza(builder)
my_pizza = pizza_operator.pizza
print(f'your pizza : {my_pizza}')
if __name__ == '__main__':
main()
#実行結果
preparing the crispy dough of your margarita, PizzaDough.crispy, None, []
adding tomato sauce...
....done adding sauce
adding topping...
....done topping
baking....
....baked!
your pizza : margarita, PizzaDough.crispy, PizzaSauce.tomato, [[<PizzaTopping.mozzarella: 2>, <PizzaTopping.oregano: 8>]]
この例ではピザマルゲリータのbuilderしかないが、設定できるデータ(enum部分)とbuilderを用意すれば、サルモーネとか、クアトロフォルマッジ等々、他のピザも作ることができる。(プログラムを追加・拡張しやすい)
いいなと思ったら応援しよう!
