はじめに
本記事は以下の続きになります。Udemyのデザインパターン講座を視聴して、学んだことを記載します。
デザインパターン
Factory Method
親クラスでインスタンスの生成方法を定め、具体的に何をどうやって作るのかは子クラスで定めるパターン。Template Methodの応用パターン。
メリット
- オープンクローズドの原則に違反することなく新しいProductを生成できる
- オブジェクトの利用側とオブジェクトの結びつきが弱くなる
使い時
- 類似した複数種類のオブジェクトを生成する必要がある場合
- オブジェクトの生成ロジックが複雑な場合
コード
# ジョブの共通インターフェース
from abc import ABC, abstractmethod
class Job(ABC):
@abstractmethod
def execute(self):
pass
# 具体的なジョブクラス
class PrintJob(Job):
def execute(self):
print("印刷ジョブを実行しています...")
class ScanJob(Job):
def execute(self):
print("スキャンジョブを実行しています...")
class CopyJob(Job):
def execute(self):
print("コピー(スキャン+印刷)ジョブを実行しています...")
# Factory Method を持つファクトリ基底クラス
class JobFactory(ABC):
@abstractmethod
def create_job(self) -> Job:
pass
# 具体的なファクトリクラス
class PrintJobFactory(JobFactory):
def create_job(self) -> Job:
return PrintJob()
class ScanJobFactory(JobFactory):
def create_job(self) -> Job:
return ScanJob()
class CopyJobFactory(JobFactory):
def create_job(self) -> Job:
return CopyJob()
def run_job(factory: JobFactory):
job = factory.create_job()
job.execute()
if __name__ == "__main__":
print("=== PRINTジョブ ===")
run_job(PrintJobFactory())
print("\n=== SCANジョブ ===")
run_job(ScanJobFactory())
print("\n=== COPYジョブ ===")
run_job(CopyJobFactory())
解説:どこがいいの?
- クライアント側(ジョブを実行する人)からは、ジョブの生成方法が見えない
→ 生成方法はカプセル化されている。中身を知ることなく、ジョブを実行するコードを組むことができる。 - 生成するオブジェクトの種類が増えたとしても、クライアント側のコードは修正が不要
→ クライアントは抽象クラスに依存しているので、新しいオブジェクトが増えたとしても、切り替え作業などは発生しない。
Facade
複雑な処理をまとめて、システムの外部に簡素化されたインターフェースを提供するパターン
メリット
- Clientに高レベルでシンプルなインターフェースを提供できる
- 複雑なクラスの構成要素を隠蔽できる
使い時
- 複雑なサブシステムの一部の機能を使用する場合
コード例
class Scanner:
def scan(self) -> str:
print("スキャン中...原稿を読み取りました")
return "scanned_image_data"
class ImageProcessor:
def process(self, image_data: str) -> str:
print("画像補正中...傾き・濃度を調整しました")
return f"processed({image_data})"
class Printer:
def print(self, processed_data: str):
print(f"印刷中...内容: {processed_data}")
class MfpFacade:
def __init__(self):
self.scanner = Scanner()
self.processor = ImageProcessor()
self.printer = Printer()
def copy(self):
image = self.scanner.scan()
processed = self.processor.process(image)
self.printer.print(processed)
if __name__ == "__main__":
mfp = MfpFacade()
print("=== コピー開始 ===")
mfp.copy()
解説
複雑な一連の処理が一つのクラスにまとまっているので、非常に読みやすいコードになります。
Prototype
既存インスタンスをコピーして新しいインスタンスを生成するパターン。
よくある分かりやすい例は、パワーポイントで全く図形を作成したいとき、1から作り直すのは面倒なので、すでに作成された図形をコピー&ペーストして作成したいですよね。それと同じで、わざわざ引数渡して新しくインスタンスを生成するよりも、既存のインスタンスを作った方が早い。そんなパターンです。ここでは2種類のコピーについて説明しておきます。
浅いコピーと深いコピー
- 浅いコピー
変数に格納された値がそのままコピーされ、オブジェクトの参照を格納した変数では参照がコピーされる。つまり、どちらかのオブジェクトを変更すると、もう片方も変更される - 深いコピー
オブジェクトの参照を格納した変数では、実体がコピーされる。つまり、どちらかのオブジェクトを変更しても、もう片方に影響を与えない。
メリット
- オブジェクトの生成処理を隠蔽できる
- 構造済みのプロトタイプのクローンを使うことで、初期化コードの重複を削減
- 利用者と具体的なクラスの結合度を弱められる
使い時
- クラスからのインスタンス生成が難しい場合
- インスタンス化のコストがコピーよりも高い場合
コード
from __future__ import annotations
import copy
from abc import ABCMeta, abstractmethod
class ItemPrototype(metaclass=ABCMeta):
def __init__(self, name:str):
self.__name= name
self.__review = list[str] = []
def __str__(self):
return f"{self.__name}: {self.__review}"
def set_review(self, review: str):
self.__review.append(review)
@abstractmethod
def create_copy(self) -> ItemPrototype:
pass
class DeepCopyItem(ItemPrototype):
def create_copy(self) -> ItemPrototype:
return copy.deepcopy(self)
class ShallowCopyItem(ItemPrototype):
def create_copy(self) -> ItemPrototype:
return copy.copy(self)
class ItemManager:
def __init__(self):
self.items = {}
def register_item(self, key: str, item: ItemPrototype)
self.items[key] = item
def create(self, key: str):
if key in self.items:
item = self.items[key]
return item.create_copy()
raise Exception("指定されたキーが見つかりません")
if __name__ == "__main__":
mouse = DeepCopyItem("マウス")
keyboard = ShallowCopyItem("キーボード")
manager = ItemManager()
manager.register_item("mouse", mouse)
manager.register_item("keyboard", keyboard)
cloned_mouse = manager.create("mouse")
cloned_keyboard = manager.create("keyboard")
cloned_mouse.set_review("Good!")
cloned_keyboard.set_review("SoSo!")
print("mouse(original): ", mouse)
print("mouse(copy): ", cloned_mouse)
print("")
print("keyboard(original): ", keyboard)
print("keyboard(copy):", cloned_keyboard)
解説
ここでは、マウスとキーボードのアイテム登録を行っています。
- マウスはディープコピーできるアイテムであり、オリジナルとコピーでは異なる出力結果が得られますが、
- キーボードはシャロ―コピーできるアイテムであり、オリジナルとコピーでは同じ出力が得られます。キーボードは同一の参照を利用しているためです。
Builderパターン
同じ生成手順で異なる材料を使って異なるオブジェクトを生成する
メリット
- 生成されるオブジェクトの生成過程や生成手段を隠すことができる
- オブジェクト構築用のコードをビジネスロジックから外すことができる
使い時
- 生成手順が同じで、詳細が異なるオブジェクトを生成する時
- 大量のパラメータをコンストラクタに渡してオブジェクトを生成している場合
コード
from abc import ABC, abstractmethod
class PrintJob:
def __init__(self):
self.paper_size = "A4"
self.color = "monochrome"
self.duplex = False
self.page_range = "all"
self.copies = 1
self.staple = False
def __str__(self):
sides = "両面" if self.duplex else "片面"
mode = "カラー" if self.color == "color" else "モノクロ"
return (
f"用紙サイズ: {self.paper_size}, {mode}, {sides}, "
f"ページ範囲: {self.page_range}, 部数: {self.copies}, "
f"ステープル: {'あり' if self.staple else 'なし'}"
)
class PrintJobBuilder(ABC):
@abstractmethod
def reset(self): pass
@abstractmethod
def set_paper_size(self): pass
@abstractmethod
def set_color(self): pass
@abstractmethod
def set_duplex(self): pass
@abstractmethod
def set_page_range(self): pass
@abstractmethod
def set_copies(self): pass
@abstractmethod
def set_staple(self): pass
@abstractmethod
def get_result(self) -> PrintJob: pass
class StandardPrintJobBuilder(PrintJobBuilder):
def __init__(self):
self.reset()
def reset(self):
self.job = PrintJob()
def set_paper_size(self):
self.job.paper_size = "A4"
def set_color(self):
self.job.color = True
def set_duplex(self, bool):
self.job.duplex = False
def set_page_range(self):
self.job.page_range = "all"
def set_copies(self, count):
self.job.copies = count
def set_staple(self):
self.job.staple = False
def get_result(self) -> PrintJob:
return self.job
class PrintJobDirector:
def __init__(self, builder: PrintJobBuilder):
self.builder = builder
def construct_standard_job(self):
self.builder.reset()
self.builder.set_paper_size()
self.builder.set_color()
self.builder.set_duplex()
self.builder.set_page_range()
self.builder.set_copies()
self.builder.set_staple()
def construct_duplex_color_job(self):
self.builder.reset()
self.builder.set_paper_size()
self.builder.set_color()
self.builder.set_duplex(self, False)
self.builder.set_page_range()
self.builder.set_copies(5)
self.builder.set_staple()
if __name__ == "__main__":
builder = StandardPrintJobBuilder()
director = PrintJobDirector(builder)
print("=== 標準印刷ジョブ ===")
director.construct_standard_job()
print(builder.get_result())
print("\n=== カラー両面 5部ジョブ ===")
director.construct_duplex_color_job()
print(builder.get_result())
解説
以上のコードでは、プリントジョブを作成オブジェクトとして、Builderを通じてDirectorが2種類のプリントジョブを生成しています。このように、オブジェクトの種類によってはデフォルトで置いておきたい変数を操作させない、という形を取ることもできます。
参考
Prototypeパターン
Builderパターン