はじめに
この記事は以下のUdemy講座を基に、学んだことを記載しています。
非常に濃厚な講座ですので、よければみなさまもご視聴ください。
※ 本記事はUdemy講座の案件記事ではありません。
デザインパターンとは
よくある設計上の問題に対する再利用可能な典型的な解決パターンのことです。先にお伝えしておくべきことは、必ずしもデザインパターンに沿って実装すれば上手くいく、ということではないことです。デザインパターンで実装するものは比較的実装コードが大きくなる場合になりますし、プロトタイプなど小規模なコードに適用すると実装時間だけかかってしまい、何の意味もなさない可能性もあります。
- 同じようなコードが 繰り返し出てきたとき
- コードの変更が別の場所にまで波及する
- チームで使うクラスの意図や構造を共有したい
こんな時にデザインパターンを利用してみても良いかも。。?と頭の中に入れておいてください。私自身は、デザインパターンの本質(パターンにどういう意味があるのか)を理解することが、生成AIを活用するものを含めたコーディング技術の向上につながる、と考えています。
本記事で学ぶデザインパターン
デザインパターンは非常に数が多いので、複数回に分けて投稿します。
今回は以下の4つを扱います。
- Template Method
- Singleton
- Adapter
- Iterator
デザインパターン
Template Method
親クラスで抽象的な関数および共通で利用する関数を定義し、子クラスで具体的な処理を書くパターン
メリット
- 共通の処理を親クラスにまとめることができるところ
- 拡張しやすい。新たに他の同等メソッドを作る場合、他の処理に関係なく拡張できる。
使い時
処理フローの全体構造を変更せず、処理の一部を修正したい場合
コード
from abc import ABC, abstractmethod
# ---- テンプレートメソッドを持つ抽象クラス -----------------------------
class JobProcessor(ABC):
def execute(self):
self.power_on()
self.prepare_device()
self.run_job()
self.cleanup()
self.power_off()
def power_on(self):
print("[共通] 電源ON")
def power_off(self):
print("[共通] 電源OFF")
@abstractmethod
def prepare_device(self):
pass
@abstractmethod
def run_job(self):
pass
def cleanup(self):
print("[共通] 後処理")
# ---- 派生クラス(PRINT) ---------------------------------------------
class PrintJobProcessor(JobProcessor):
def prepare_device(self):
print("→ 用紙とトナーをチェック")
def run_job(self):
print("→ 印刷データをレンダリングし印刷")
# ---- 派生クラス(SCAN) ----------------------------------------------
class ScanJobProcessor(JobProcessor):
def prepare_device(self):
print("→ 原稿台またはADFをチェック")
def run_job(self):
print("→ 原稿をスキャンしてPDF保存")
# ---- 実行デモ ---------------------------------------------------------
if __name__ == "__main__":
print("=== PRINTジョブ ===")
job = PrintJobProcessor()
job.execute()
print("\n=== SCANジョブ ===")
job = ScanJobProcessor()
job.execute()
Singletonパターン
インスタンスを1つだけに制限したいときに使うパターン。現在はアンチパターンと言われることが多い。
メリット
- 自分自身のインスタンスを内部に保持して管理
- どの処理からもログが一元管理され、状況共有が簡単
- 明示的なアクセス設計になる
どうしてアンチパターンなのか
昔のスペックのPCではメモリをあまり使わない点で有効であった。現在のスペックのPCではインスタンスの数によってメモリを圧迫されることは少ないので、インスタンスを1つに制限するメリットが大きくない。
コード
import threading
class Logger:
_instance = None
_lock = threading.Lock()
def __init__(self):
if Logger._instance is not None:
raise Exception("Use get_instance() instead.")
self.logs = []
@classmethod
def get_instance(cls) -> "Logger":
with cls._lock:
if cls._instance is None:
cls._instance = Logger()
return cls._instance
def log(self, message: str):
self.logs.append(message)
print(f"[LOG] {message}")
def get_all_logs(self):
return self.logs
※ 補足:new メソッド
インスタンスが作成される前に呼ばれるメソッド
Adapterパターン
あるクラスのインターフェースを別のクラスで利用するためのインターフェースに変換するためのパターン
メリット
- ユーザからはターゲットのクラスしか見えない
- 既存のクラスを修正せずに使える
使い時
- 元のクラスが過去に十分にテストされていて、変更を加えずにカスタマイズしたい時
コード
### 共通インターフェース(MFP側が依存する) ###
class CloudUploader:
def upload(self, filename: str, content: bytes):
raise NotImplementedError
### クラウドサービス(API仕様がバラバラ) ###
class GoogleDriveAPI:
def auth(self): print("Google認証完了")
def push_file(self, file_obj: dict):
print(f"GoogleDriveにアップロード: {file_obj['name']}")
class DropboxAPI:
def connect(self): print("Dropbox接続OK")
def upload_binary(self, path: str, data: bytes):
print(f"Dropboxにアップロード: {path}")
### アダプター(CloudUploaderに適合させる) ###
class GoogleDriveAdapter(CloudUploader):
def __init__(self): self.drive = GoogleDriveAPI()
def upload(self, filename: str, content: bytes):
self.drive.auth()
self.drive.push_file({"name": filename, "data": content})
class DropboxAdapter(CloudUploader):
def __init__(self): self.dbx = DropboxAPI()
def upload(self, filename: str, content: bytes):
self.dbx.connect()
self.dbx.upload_binary(f"/MFP/{filename}", content)
### MFPのスキャン処理(Uploaderを使う側) ###
class ScanJobUploader:
def __init__(self, uploader: CloudUploader):
self.uploader = uploader
def execute(self):
filename = "scanned_doc.pdf"
content = b"%PDF ...binary data..."
print(f"[MFP] スキャン完了: {filename}")
self.uploader.upload(filename, content)
Iterator
コレクションの内部構造を利用者が見せずに、その要素に順番にアクセスする方法を提供するパターン。ループ処理のインデックスiの役割を抽象化し、一般化したもの。
メリット
- 利用者がコレクションの詳細なデータ構造を知る必要がなくなる
- コレクションの実装と探索のためのアルゴリズムは分離することができる
- 既存のコードに修正を加えることなく、新しい種類のコレクションやイテレータを追加できる
使い時
- コレクションが複雑な構造をしているとき
- 複数の探索方法を実装したいとき
コード
from typing import List, Protocol
# ---- インターフェース (Aggregate / Iterator) ----------------------------
class Iterator(Protocol):
def has_next(self) -> bool: ...
def next(self): ...
class Aggregate(Protocol):
def get_iterator(self) -> Iterator: ...
# ---- ジョブエンティティ --------------------------------------------------
class Job:
def __init__(self, job_id: int, kind: str, pages: int):
self.job_id = job_id
self.kind = kind # 例: "PRINT", "SCAN", "COPY"
self.pages = pages
def __repr__(self):
return f"Job(id={self.job_id}, kind={self.kind}, pages={self.pages})"
# ---- 集約クラス:ジョブキュー -------------------------------------------
class JobQueue(Aggregate):
def __init__(self):
self._jobs: List[Job] = []
def add_job(self, job: Job) -> None:
self._jobs.append(job)
def get_count(self) -> int:
return len(self._jobs)
def get_jobs(self) -> List[Job]:
return list(self._jobs)
def get_iterator(self) -> "JobQueueIterator":
return JobQueueIterator(self)
# ---- イテレータ:ジョブキュー走査 ---------------------------------------
class JobQueueIterator(Iterator):
def __init__(self, queue: JobQueue):
self._queue = queue
self._position = 0
def has_next(self) -> bool:
return self._position < self._queue.get_count()
def next(self) -> Job:
if not self.has_next():
raise StopIteration("No more jobs.")
job = self._queue.get_jobs()[self._position]
self._position += 1
return job
# for文にも対応
def __iter__(self): return self
def __next__(self): return self.next()
# ---- デモ実行 ------------------------------------------------------------
if __name__ == "__main__":
queue = JobQueue()
queue.add_job(Job(1, "PRINT", 10))
queue.add_job(Job(2, "COPY", 5))
queue.add_job(Job(3, "SCAN", 2))
print("=== MFPジョブ一覧 ===")
it = queue.get_iterator()
while it.has_next():
print(it.next())
print("\n=== for文でジョブ走査 ===")
for job in queue.get_iterator():
print(f"Job Type: {job.kind}, Pages: {job.pages}")