Gofデザインパターン 覚えておきたい9つ (python)
はじめに
よく使うと言われているGofデザインパターンをpythonを用いて、理解しようというものです。
自身の理解のために、まとめたものです。
Factory Method
オブジェクトの生成をサブクラスに任せることで、コードの柔軟性や拡張性を向上させるためのパターンです。
コードの例
乗り物を例に挙げてみます。
from abc import ABC, abstractmethod
# インターフェース
class Vehicle(ABC):
@abstractmethod
def get_type(self):
pass
class VehicleFactory(ABC):
@abstractmethod
def create_vehicle(self):
pass
# サブクラス
class Car(Vehicle):
def get_type(self):
return "I am a Car"
class Bike(Vehicle):
def get_type(self):
return "I am a Bike"
# ファクトリークラス(サブクラスを生成する)
class CarFactory(VehicleFactory):
def create_vehicle(self):
return Car()
class BikeFactory(VehicleFactory):
def create_vehicle(self):
return Bike()
def main():
car_factory = CarFactory() # 車の工場
car = car_factory.create_vehicle()
print(car.get_type()) # "I am a Car"
bike_factory = BikeFactory() # 自転車の工場
bike = bike_factory.create_vehicle()
print(bike.get_type()) # "I am a Bike"
メリット
-
新しい乗り物の追加が簡単
例)バスを追加したい場合
・Bus()
・BusFactory() -
依存性の削減
クライアントコード(main関数)は具体的なクラス(Car,Bike等)を直接参照しないため、拡張が簡単。
発展内容
ファクトリークラスとサブクラスをそれぞれ新規追加するという修正箇所が多いので、共通部品化してみたコードが次のコードです。
from abc import ABC, abstractmethod
# インターフェース
class Vehicle(ABC):
@abstractmethod
def get_name(self) -> str: # 乗り物を返す
pass
@abstractmethod
def get_sound(self) -> str: # 動いたときの音を返す
pass
# サブクラス
class Car(Vehicle):
def get_name(self) -> str:
return "Car"
def get_sound(self) -> str:
return "Vroom Vroom"
class Bike(Vehicle):
def get_name(self) -> str:
return "Bike"
def get_sound(self) -> str:
return "Ring Ring"
class Bus(Vehicle):
def get_name(self) -> str:
return "Bus"
def get_sound(self) -> str:
return "Honk Honk"
# Factoryクラス
class VehicleFactory:
_vehicle_map = {
"car": Car,
"bike": Bike,
"bus": Bus,
}
@staticmethod
def create_vehicle(vehicle_type: str) -> Vehicle:
try:
return VehicleFactory._vehicle_map[vehicle_type]()
except KeyError:
raise ValueError(f"Unknown vehicle type: {vehicle_type}")
def main():
car = VehicleFactory.create_vehicle("car") # 車を生成
print(f"Name: {car.get_name()}, Sound: {car.get_sound()}")
bike = VehicleFactory.create_vehicle("bike") # 自転車を生成
print(f"Name: {bike.get_name()}, Sound: {bike.get_sound()}")
bus = VehicleFactory.create_vehicle("bus") # バスを生成
print(f"Name: {bus.get_name()}, Sound: {bus.get_sound()}")
# 出力結果
Name: Car, Sound: Vroom Vroom
Name: Bike, Sound: Ring Ring
Name: Bus, Sound: Honk Honk
メンテナンス時には サブクラス と _vehicle_map の2箇所を修正すれば対応可能です。この構造により、コードの拡張性が高く、メンテナンスが容易になります。
Command
操作(命令)をオブジェクトとしてカプセル化し、それを実行する方法を柔軟に変更できるデザインパターンです。
■役割
・Invoker(呼び出し元): 操作を抽象化しておきたい(例: リモコンやアプリのUI)。
・Commandオブジェクト: 操作(drive, stop)を具体的に実装し、Invokerから利用できるようにする。
・Receiver(受信者): 操作される「Car」。
コードの例
自動運転でリモコンで操作することを想定して例を挙げてみます。
# インターフェース
class Command:
def execute(self):
pass
def undo(self):
pass
# Receiver(操作される側)
class Car:
def drive(self):
print("Car is driving.")
def stop(self):
print("Car has stopped.")
# Commandクラス
class DriveCommand(Command):
def __init__(self, vehicle):
self.vehicle = vehicle
def execute(self):
self.vehicle.drive()
def undo(self):
self.vehicle.stop()
class StopCommand(Command):
def __init__(self, vehicle):
self.vehicle = vehicle
def execute(self):
self.vehicle.stop()
def undo(self):
print("No action for undo after stopping.")
# Invoker(呼び出し元)
class CarController:
def __init__(self):
self.history = []
def set_command(self, command):
self.command = command
def press_button(self):
self.command.execute()
self.history.append(self.command)
def press_undo(self):
if self.history:
last_command = self.history.pop()
last_command.undo()
def main():
my_car = Car()
# コマンドを作成
drive_car = DriveCommand(my_car)
stop_car = StopCommand(my_car)
# リモコンにコマンドを設定
controller = CarController()
# 車を操作
controller.set_command(drive_car)
controller.press_button() # Car is driving.
controller.set_command(stop_car)
controller.press_button() # Car has stopped.
メリット
-
コードの疎結合化
Invoker(呼び出し元)とReceiver(操作される対象)が疎結合になることで、テスト容易性・保守性が高まります。
車を「動かす」「止める」「クラクションを鳴らす」といった操作が、それぞれ独立したコマンドとして実装されます。そのため、操作を柔軟に追加・変更できる設計にできます。 -
操作のカプセル化
1つの操作(機能)を「Commandオブジェクト」にまとめることで、操作を独立させ、柔軟に扱えるようにできます。 -
操作の追加が容易
新しい操作を追加する際に既存のコードを変更する必要がほとんどありません。新しいCommandクラスを作成し、Invokerで使うだけです。
Observer
「オブジェクト間の一対多の依存関係」を構築するためのデザインパターンです。
1つのオブジェクト(Subjectと呼ばれる)に変更があると、それに依存する他のオブジェクト(Observersと呼ばれる)が自動的に通知を受けます。
コードの例
ニュース(Subject)が更新されると、登録された購読者(Observers)に通知される例です。
# Subject (ニュース発信者)
class NewsPublisher:
def __init__(self):
self.subscribers = [] # 観察者リスト
def subscribe(self, observer):
self.subscribers.append(observer) # 購読者を登録
def unsubscribe(self, observer):
self.subscribers.remove(observer) # 購読者を削除
def notify(self, news):
for subscriber in self.subscribers:
subscriber.update(news) # 購読者全員に通知
# Observer (購読者)
class Subscriber:
def __init__(self, name):
self.name = name
def update(self, news):
print(f"{self.name} さんがニュースを受け取りました: {news}") # 通知を受け取る
def main():
news_publisher = NewsPublisher()
alice = Subscriber("Alice")
bob = Subscriber("Bob")
charlie = Subscriber("Charlie")
news_publisher.subscribe(alice)
news_publisher.subscribe(bob)
# ニュースを配信
news_publisher.notify("Python 3.12がリリースされました!")
# Charlieを追加で購読
news_publisher.subscribe(charlie)
news_publisher.notify("ChatGPTの新機能が登場しました!")
# Bobの購読解除
news_publisher.unsubscribe(bob)
news_publisher.notify("次回のアップデートは来月です!")
Alice さんがニュースを受け取りました: Python 3.12がリリースされました!
Bob さんがニュースを受け取りました: Python 3.12がリリースされました!
Alice さんがニュースを受け取りました: ChatGPTの新機能が登場しました!
Bob さんがニュースを受け取りました: ChatGPTの新機能が登場しました!
Charlie さんがニュースを受け取りました: ChatGPTの新機能が登場しました!
Alice さんがニュースを受け取りました: 次回のアップデートは来月です!
Charlie さんがニュースを受け取りました: 次回のアップデートは来月です!
メリット
-
コードの疎結合化
SubjectはObserversの具体的内容を理解しなくても通知が可能です。
また、両者とも直接依存しないため、保守性が高まります。 -
変更に強い
新しいObserver(購読者)を追加しても、既存のSubjectやObserverに変更を加える必要がありません。 -
イベント駆動型プログラミングに最適
Observerパターンはイベント通知の仕組みを簡潔に実装するため、イベント駆動型のプログラムに適しています。
拡張例
新しい購読者タイプを簡単に追加できます。たとえば、SMS購読者やメール購読者を追加しても、既存のSubjectを変更する必要はありません。
class SMSSubscriber(Subscriber):
def update(self, news):
print(f"SMSで通知: {self.name}さんへニュース - {news}")
class EmailSubscriber(Subscriber):
def update(self, news):
print(f"Emailで通知: {self.name}さんへニュース - {news}")
Strategy
アルゴリズムの部分を独立したクラスとして定義し、実行時にそれを切り替えることで柔軟に処理を変更できるデザインパターンです。
コードの例
割引ロジック(アルゴリズム部分)を独立したクラスとして、定義した例です。
# 各アルゴリズムのクラス
class FixedDiscount:
def __init__(self, amount):
self.amount = amount
def calculate(self, total_price):
return max(0, total_price - self.amount)
class PercentageDiscount:
def __init__(self, percentage):
self.percentage = percentage
def calculate(self, total_price):
return max(0, total_price * (1 - self.percentage / 100))
class NoDiscount:
def calculate(self, total_price):
return total_price
# Strategyクラス
class ShoppingCart:
def __init__(self, total_price, discount_strategy):
self.total_price = total_price
self.discount_strategy = discount_strategy
def apply_discount(self):
return self.discount_strategy.calculate(self.total_price)
def main():
# ショッピングカートを作成
cart1 = ShoppingCart(1000, FixedDiscount(200)) # 固定額割引
cart2 = ShoppingCart(1000, PercentageDiscount(10)) # 割合割引
cart3 = ShoppingCart(1000, NoDiscount()) # 割引なし
print(cart1.apply_discount()) # 800
print(cart2.apply_discount()) # 900
print(cart3.apply_discount()) # 1000
メリット
- 拡張性が高い
クラスが独立しているため、アルゴリズムの追加や既存のアルゴリズムの変更が容易になります。
オープン・クローズドの原則(SOLID原則のひとつ) - テストが容易
アルゴリズムの独立したクラスのテストができるため、単体でテストがしやすいです。
Adapter
互換性のないクラス同士をつなげるためのデザインパターンです。
コードの例
「既存の古いシステム」と「新しいシステム」のインターフェースを統一する例です。
# 古いプリンタクラス
class OldPrinter:
def print_text(self, text):
print(f"OldPrinter: {text}")
# 新しいプリンタのインターフェース
class NewPrinterInterface:
def print(self, text):
pass
# Adapterクラス
class PrinterAdapter(NewPrinterInterface):
def __init__(self, old_printer):
self.old_printer = old_printer
def print(self, text):
self.old_printer.print_text(text) # 古いプリンタのメソッドを呼び出す
def main():
old_printer = OldPrinter()
adapter = PrinterAdapter(old_printer) # Adapterを使って古いプリンタを新しいインターフェースに適合
adapter.print("Hello, world!") # NewPrinterInterfaceと互換性のある形で動作する
メリット
-
既存コードの変更不要
既存クラスを変更せずに、新しいシステムに適応できます。 -
互換性の向上
異なるインターフェース同士を接続できるようにします。 -
拡張性
新しいインターフェースを採用する場合でも、既存コードを再利用できます。
Singleton
クラスから生成されるインスタンスが常に1つだけであることを保証するデザインパターンです。
利用ケース
設定情報の管理: 設定を保持するクラスが複数存在すると不整合が起こるため、一元管理したい。
リソースの節約: データベース接続のような重い処理を複数回実行しないようにする。
コードの例
class ConfigManager:
_instance = None # クラス変数で唯一のインスタンスを保持する
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls) # インスタンスが未生成の場合のみインスタンスを生成する
cls._instance.settings = {} # 設定情報を保持する辞書を初期化
return cls._instance
def set(self, key, value):
self.settings[key] = value
def get(self, key):
return self.settings.get(key, None)
def main():
config1 = ConfigManager()
config2 = ConfigManager()
# 設定を追加
config1.set("theme", "dark")
config1.set("language", "English")
# 別の場所から設定を取得
print(config2.get("theme")) # dark
print(config2.get("language")) # English
# 同じインスタンスを参照していることを確認
print(config1 is config2) # True
メリット
- 一貫性を保つ
インスタンスが1つだけなので、データの不整合が起こりにくいです。 - リソースの節約
データベース接続やネットワークリソースを1回だけ確保し、無駄な処理を減らすことができます。 - グローバルアクセスが可能
どこからでもアクセス可能なため、アプリケーション全体で管理したいもので有効です。
Facade
複雑なシステムの一連の処理を簡単に利用できるようにするためのデザインパターンです。
コードの例
ホームシアターのシステムを例に挙げます。
直接サブシステムを操作せず、Facade(窓口)を利用するのがポイントです。
# システムの処理 (サブシステム)
class Projector:
def on(self):
print("プロジェクターの電源を入れました。")
class Speaker:
def on(self):
print("スピーカーの電源を入れました。")
class BluRayPlayer:
def on(self):
print("Blu-rayプレイヤーの電源を入れました。")
def play(self, movie):
print(f"『{movie}』を再生します。")
# Facade
class HomeTheaterFacade:
def __init__(self, projector, speaker, blu_ray):
self.projector = projector
self.speaker = speaker
self.blu_ray = blu_ray
def watch_movie(self, movie):
print("\n映画を再生します...")
self.projector.on()
self.speaker.on()
self.blu_ray.on()
self.blu_ray.play(movie)
print("映画をお楽しみください!")
def main():
projector = Projector()
speaker = Speaker()
blu_ray = BluRayPlayer()
home_theater = HomeTheaterFacade(projector, speaker, blu_ray)
home_theater.watch_movie("タイタニック")
映画を再生します...
プロジェクターの電源を入れました。
スピーカーの電源を入れました。
Blu-rayプレイヤーの電源を入れました。
『タイタニック』を再生します。
映画をお楽しみください!
メリット
- シンプルなインターフェース
watch_movie()だけを呼び出せば、実行できます。 - 変更に強い
サブシステムが変わっても、Facade を変更するだけで済みます。 - 再利用しやすい
他のシステムでも HomeTheaterFacade をそのまま使えます。
Template Method
処理の枠組み(テンプレート)をスーパークラスに定義し、一部の詳細な処理をサブクラスで実装するデザインパターンです。
コードの例
レポートの作成の処理を例に挙げます。
from abc import ABC, abstractmethod
# インターフェースではなく、抽象クラス
class ReportTemplate(ABC):
def create_report(self): # レポート作成の流れを定義
self.gather_data() # 抽象メソッド
self.analyze_data() # 抽象メソッド
self.format_report() # 抽象メソッド
self.print_report() # 具象メソッド(共通処理)
@abstractmethod
def gather_data(self): # データ収集
pass
@abstractmethod
def analyze_data(self): # データ分析
pass
@abstractmethod
def format_report(self): # レポートのフォーマット
pass
def print_report(self): # レポートを印刷
print("レポートを印刷しました!")
# 売上レポート
class SalesReport(ReportTemplate):
def gather_data(self):
print("売上データを収集しました。")
def analyze_data(self):
print("売上データを分析しました。")
def format_report(self):
print("売上レポートをフォーマットしました。")
# 従業員レポート
class EmployeeReport(ReportTemplate):
def gather_data(self):
print("従業員データを収集しました。")
def analyze_data(self):
print("従業員データを分析しました。")
def format_report(self):
print("従業員レポートをフォーマットしました。")
def main():
# 売上レポートを作成
sales_report = SalesReport()
sales_report.create_report()
print("\n=================\n")
# 従業員レポートを作成
employee_report = EmployeeReport()
employee_report.create_report()
メリット
- 共通の処理をスーパークラスで定義し、繰り返し記述を防ぐ
「レポート作成」の処理で、データの取得・分析・フォーマットが異なっても、全体の流れは変わらないため、処理を共通化できます。 - コードの修正が容易
例えば、print_report()を修正したい場合、スーパークラスだけ修正すればOKです。 - 新しいサブクラスの追加が容易
新しいレポートを追加したい場合に、スーパークラスを変更せずに実装できます。 - 一部の処理を変更しやすい
Template Methodを使うと 必要な部分だけオーバーライドできます。
参考記事
Discussion