見出し画像

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を用意すれば、サルモーネとか、クアトロフォルマッジ等々、他のピザも作ることができる。(プログラムを追加・拡張しやすい

もっと応援したいなと思っていただけた場合、よろしければサポートをおねがいします。いただいたサポートは活動費に使わせていただきます。