TechHub

エンジニアの成長をサポートする技術情報サイト

← 記事一覧に戻る

GoFの23のデザインパターン完全ガイド

公開日: 2024年2月13日 著者: mogura
GoFの23のデザインパターン完全ガイド

疑問

GoFの23のデザインパターンとは何で、それぞれどのような特徴があるのでしょうか?生成パターン、構造パターン、振る舞いパターンについて一緒に学んでいきましょう。

導入

GoF(Gang of Four)の23のデザインパターンは、オブジェクト指向設計の基礎となっている、最も重要なデザインパターンの集合です。1994年に出版された「デザインパターン」という書籍で紹介され、現在でもソフトウェア開発の標準的な設計手法として広く使用されています。

23のパターンは、生成に関するパターン(Creational Patterns)、構造に関するパターン(Structural Patterns)、振る舞いに関するパターン(Behavioral Patterns)の3つのカテゴリに分類されます。本記事では、各パターンの特徴と実践的な使用例を詳しく解説していきます。

GoFの23のデザインパターン

解説

1. デザインパターンの分類

GoFの23のデザインパターンは、生成、構造、振る舞いの3つのカテゴリに分類されます。各カテゴリは異なる設計上の問題を解決します。

3つのカテゴリ

  • 生成に関するパターン(Creational Patterns): オブジェクトの生成方法を抽象化し、柔軟で再利用可能な方法を提供します。Singleton、Factory、Abstract Factory、Builder、Prototypeの5つのパターンが含まれます。
  • 構造に関するパターン(Structural Patterns): クラスやオブジェクトを組み合わせて、より大きな構造を構築する方法を定義します。Adapter、Bridge、Composite、Decorator、Facade、Flyweight、Proxyの7つのパターンが含まれます。
  • 振る舞いに関するパターン(Behavioral Patterns): オブジェクト間の通信と責任の分担を定義します。Chain of Responsibility、Command、Interpreter、Iterator、Mediator、Memento、Observer、State、Strategy、Template Method、Visitorの11のパターンが含まれます。

2. 生成に関するパターン(Creational Patterns)

生成に関するパターンは、オブジェクトの生成方法を抽象化し、柔軟で再利用可能な方法を提供します。Singleton、Factory、Abstract Factory、Builder、Prototypeの5つのパターンがあります。

Singleton(シングルトン)

Singletonパターンは、クラスのインスタンスを1つだけに制限するパターンです。グローバルなアクセスポイントを提供し、リソースの共有を可能にします。ただし、テストが困難になる可能性があるため、慎重に使用する必要があります。

Factory(ファクトリー)

Factoryパターンは、オブジェクトの生成を専用のクラスに委譲するパターンです。クライアントコードは具体的なクラス名を知る必要がなく、インターフェースを通じてオブジェクトを使用できます。これにより、実装の詳細を隠蔽し、拡張性を向上させます。

Abstract Factory(抽象ファクトリー)

Abstract Factoryパターンは、関連するオブジェクトのファミリーを生成するためのインターフェースを提供します。例えば、GUIアプリケーションで、Windows用とMac用のボタンやメニューを一貫した方法で生成できます。

Builder(ビルダー)

Builderパターンは、複雑なオブジェクトを段階的に構築するパターンです。多くのパラメータを持つオブジェクトの生成を簡潔にし、可読性を向上させます。オプショナルなパラメータが多い場合に特に有効です。

Prototype(プロトタイプ)

Prototypeパターンは、既存のオブジェクトをコピーして新しいオブジェクトを作成するパターンです。オブジェクトの生成コストが高い場合や、実行時に生成するクラスを決定する場合に有効です。

SingletonとFactoryパターンの実装例

Singletonパターンでは、クラスのインスタンスを1つだけに制限しています。Factoryパターンでは、オブジェクトの生成を専用のクラスに委譲し、クライアントコードは具体的なクラス名を知る必要がありません。

# Singletonパターンの例
class DatabaseConnection:
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance.connected = False
        return cls._instance
    
    def connect(self):
        if not self.connected:
            print("データベースに接続しました")
            self.connected = True
    
    def disconnect(self):
        if self.connected:
            print("データベース接続を切断しました")
            self.connected = False

# 使用例
conn1 = DatabaseConnection()
conn2 = DatabaseConnection()
print(conn1 is conn2)  # True - 同じインスタンス

# Factoryパターンの例
from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def speak(self):
        pass

class Dog(Animal):
    def speak(self):
        return "ワンワン"

class Cat(Animal):
    def speak(self):
        return "ニャーニャー"

class AnimalFactory:
    @staticmethod
    def create_animal(animal_type: str) -> Animal:
        if animal_type == "dog":
            return Dog()
        elif animal_type == "cat":
            return Cat()
        else:
            raise ValueError(f"Unknown animal type: {animal_type}")

# 使用例
animal = AnimalFactory.create_animal("dog")
print(animal.speak())  # ワンワン

3. 構造に関するパターン(Structural Patterns)

構造に関するパターンは、クラスやオブジェクトを組み合わせて、より大きな構造を構築する方法を定義します。Adapter、Bridge、Composite、Decorator、Facade、Flyweight、Proxyの7つのパターンがあります。

Adapter(アダプター)

Adapterパターンは、互換性のないインターフェースを持つクラスを、既存のコードと連携できるようにするパターンです。既存のライブラリを新しいシステムに統合する際や、レガシーコードを新しいインターフェースに適合させる際に使用します。

Bridge(ブリッジ)

Bridgeパターンは、実装と抽象化を分離し、独立して変更できるようにするパターンです。継承の代わりにコンポジションを使用することで、柔軟性を向上させ、クラス階層の爆発的な増加を防ぎます。

Composite(コンポジット)

Compositeパターンは、個々のオブジェクトとオブジェクトのグループを同じように扱えるようにするパターンです。ファイルシステムやUIコンポーネントなど、ツリー構造を表現する際に使用します。

Decorator(デコレーター)

Decoratorパターンは、オブジェクトに動的に機能を追加するパターンです。継承を使わずに、実行時にオブジェクトの機能を拡張できます。ストリーム処理やUIコンポーネントの装飾などで使用されます。

Facade(ファサード)

Facadeパターンは、複雑なサブシステムに対して、シンプルなインターフェースを提供するパターンです。クライアントコードを簡潔にし、サブシステムの複雑さを隠蔽します。ライブラリのAPI設計でよく使用されます。

Flyweight(フライウェイト)

Flyweightパターンは、大量のオブジェクトを効率的に共有するパターンです。共通の状態を共有し、固有の状態のみを個別に保持することで、メモリ使用量を削減し、パフォーマンスを向上させます。

Proxy(プロキシ)

Proxyパターンは、他のオブジェクトへのアクセスを制御するための代理オブジェクトを提供するパターンです。遅延読み込み、アクセス制御、ロギング、キャッシングなどに使用されます。

AdapterとDecoratorパターンの実装例

Adapterパターンでは、古いシステムのインターフェースを新しいインターフェースに適合させています。Decoratorパターンでは、コーヒーに動的にミルクや砂糖を追加できるようにしています。

# Adapterパターンの例
class OldSystem:
    def old_method(self):
        return "古いシステムのメソッド"

class NewInterface:
    def new_method(self):
        pass

class Adapter(NewInterface):
    def __init__(self, old_system: OldSystem):
        self.old_system = old_system
    
    def new_method(self):
        return self.old_system.old_method()

# Decoratorパターンの例
class Coffee:
    def cost(self):
        return 5
    
    def description(self):
        return "コーヒー"

class CoffeeDecorator(Coffee):
    def __init__(self, coffee: Coffee):
        self.coffee = coffee
    
    def cost(self):
        return self.coffee.cost()
    
    def description(self):
        return self.coffee.description()

class MilkDecorator(CoffeeDecorator):
    def cost(self):
        return self.coffee.cost() + 2
    
    def description(self):
        return self.coffee.description() + "、ミルク"

class SugarDecorator(CoffeeDecorator):
    def cost(self):
        return self.coffee.cost() + 1
    
    def description(self):
        return self.coffee.description() + "、砂糖"

# 使用例
coffee = Coffee()
coffee_with_milk = MilkDecorator(coffee)
coffee_with_milk_and_sugar = SugarDecorator(coffee_with_milk)
print(coffee_with_milk_and_sugar.description())  # コーヒー、ミルク、砂糖
print(coffee_with_milk_and_sugar.cost())  # 8

4. 振る舞いに関するパターン(Behavioral Patterns)

振る舞いに関するパターンは、オブジェクト間の通信と責任の分担を定義します。Chain of Responsibility、Command、Interpreter、Iterator、Mediator、Memento、Observer、State、Strategy、Template Method、Visitorの11のパターンがあります。

Chain of Responsibility(責任の鎖)

Chain of Responsibilityパターンは、リクエストを処理するオブジェクトのチェーンを構築するパターンです。イベント処理や例外処理などで使用されます。

Command(コマンド)

Commandパターンは、リクエストをオブジェクトとしてカプセル化するパターンです。UIのボタン操作や、Undo/Redo機能の実装などで使用されます。

Interpreter(インタープリター)

Interpreterパターンは、言語の文法を表現し、その言語で書かれた文を解釈するパターンです。構文解析や式の評価などで使用されますが、複雑な言語には適していません。

Iterator(イテレーター)

Iteratorパターンは、コレクションの要素に順次アクセスする方法を提供するパターンです。多くのプログラミング言語で標準ライブラリに組み込まれています。

Mediator(メディエーター)

Mediatorパターンは、オブジェクト間の通信を仲介するオブジェクトを提供するパターンです。UIコンポーネント間の通信などで使用されます。

Memento(メメント)

Mementoパターンは、オブジェクトの内部状態を保存し、後で復元できるようにするパターンです。Undo機能やスナップショット機能の実装に使用されます。

Observer(オブザーバー)

Observerパターンは、オブジェクトの状態変化を、依存している他のオブジェクトに通知するパターンです。イベント駆動システムやMVCアーキテクチャで広く使用されます。

State(ステート)

Stateパターンは、オブジェクトの内部状態に応じて、動作を変更できるようにするパターンです。状態遷移を管理する際に使用され、if-else文の代わりにポリモーフィズムを活用します。

Strategy(ストラテジー)

Strategyパターンは、アルゴリズムをカプセル化し、実行時に選択できるようにするパターンです。if-else文の代わりに、ポリモーフィズムを活用して柔軟な設計を実現します。

Template Method(テンプレートメソッド)

Template Methodパターンは、アルゴリズムの骨格を定義し、一部のステップをサブクラスで実装するパターンです。共通の処理を基底クラスに集約し、コードの重複を削減します。

Visitor(ビジター)

Visitorパターンは、オブジェクト構造の要素に対して操作を実行するパターンです。新しい操作を追加する際に、既存のクラスを変更する必要がなくなり、拡張性が向上します。

StrategyとCommandパターンの実装例

Strategyパターンでは、支払い方法をカプセル化し、実行時に選択できるようにしています。Commandパターンでは、リクエストをオブジェクトとしてカプセル化し、Undo機能を実装しています。

# Strategyパターンの例
from abc import ABC, abstractmethod

class PaymentStrategy(ABC):
    @abstractmethod
    def pay(self, amount: float):
        pass

class CreditCardPayment(PaymentStrategy):
    def pay(self, amount: float):
        print(f"クレジットカードで{amount}円を支払います")

class PayPalPayment(PaymentStrategy):
    def pay(self, amount: float):
        print(f"PayPalで{amount}円を支払います")

class PaymentProcessor:
    def __init__(self, strategy: PaymentStrategy):
        self.strategy = strategy
    
    def set_strategy(self, strategy: PaymentStrategy):
        self.strategy = strategy
    
    def process_payment(self, amount: float):
        self.strategy.pay(amount)

# Commandパターンの例
class Command(ABC):
    @abstractmethod
    def execute(self):
        pass
    
    @abstractmethod
    def undo(self):
        pass

class Light:
    def __init__(self):
        self.is_on = False
    
    def turn_on(self):
        self.is_on = True
        print("ライトが点灯しました")
    
    def turn_off(self):
        self.is_on = False
        print("ライトが消灯しました")

class LightOnCommand(Command):
    def __init__(self, light: Light):
        self.light = light
    
    def execute(self):
        self.light.turn_on()
    
    def undo(self):
        self.light.turn_off()

class RemoteControl:
    def __init__(self):
        self.command = None
    
    def set_command(self, command: Command):
        self.command = command
    
    def press_button(self):
        if self.command:
            self.command.execute()
    
    def press_undo(self):
        if self.command:
            self.command.undo()

# 使用例
light = Light()
light_on = LightOnCommand(light)
remote = RemoteControl()
remote.set_command(light_on)
remote.press_button()  # ライトが点灯しました
remote.press_undo()  # ライトが消灯しました

5. パターンの選択方法

適切なデザインパターンを選択するには、問題の性質を理解し、各パターンの利点と欠点を考慮する必要があります。問題が生成、構造、振る舞いのどのカテゴリに該当するかを判断することが重要です。

パターン選択の指針

  • 問題のカテゴリを判断する: 解決しようとしている問題が、生成、構造、振る舞いのどのカテゴリに該当するかを判断します。オブジェクトの生成に関する問題なら生成パターン、オブジェクト間の関係に関する問題なら構造パターン、オブジェクト間の通信に関する問題なら振る舞いパターンを検討します。
  • パターンの目的を理解する: 各パターンが解決しようとしている問題と、その解決方法を理解します。パターンの目的と、現在のプロジェクトの要件が一致しているかを確認します。
  • トレードオフを考慮する: 各パターンには利点と欠点があります。コードの複雑さ、パフォーマンス、保守性などのトレードオフを考慮し、最適なパターンを選択します。
  • シンプルさを優先する: シンプルな解決策で問題が解決できる場合は、デザインパターンを使用する必要はありません。パターンは問題を解決するための手段であり、目的ではありません。

パターンの組み合わせ

複数のデザインパターンを組み合わせて使用することもできます。例えば、FactoryパターンとStrategyパターンを組み合わせることで、実行時に異なる戦略を生成し、選択できるようにできます。ただし、過度な組み合わせは避け、必要に応じて使用します。

6. 実践的な使用例

デザインパターンは、実際のプロジェクトで使用することで、その効果を実感できます。Webアプリケーション、ゲーム開発、フレームワーク開発など、様々な場面でデザインパターンが活用されています。

よく使用されるパターン

  • Webアプリケーション開発: MVCアーキテクチャでObserverパターン、リクエスト処理でChain of Responsibilityパターン、データアクセス層でRepositoryパターン(GoFのパターンではないが、よく使用される)などが使用されます。
  • ゲーム開発: ゲームオブジェクトの生成でFactoryパターン、状態管理でStateパターン、コマンド処理でCommandパターンなどが使用されます。
  • フレームワーク開発: プラグインシステムでStrategyパターン、イベント処理でObserverパターン、オブジェクト生成でFactoryパターンなどが使用されます。
  • ライブラリ開発: API設計でFacadeパターン、オブジェクトの拡張でDecoratorパターン、アクセス制御でProxyパターンなどが使用されます。

7. ベストプラクティス

デザインパターンを効果的に活用するには、適切なタイミングで使用し、過度な使用を避け、コードの可読性と保守性を優先することが重要です。

ベストプラクティス

  • 問題が存在する場合のみ使用する: デザインパターンは、問題を解決するための手段です。問題が存在しない場合や、シンプルな解決策で十分な場合は、パターンを使用する必要はありません。
  • パターンの名前を明確にする: デザインパターンを使用している箇所は、コメントやドキュメントで明確に示します。どのパターンを使用しているのか、なぜそのパターンを選択したのかを記録することで、将来のメンテナンスが容易になります。
  • チームの理解度を考慮する: チームメンバーがパターンを理解しているかどうかを考慮します。理解されていないパターンを使用すると、コードの可読性が低下する可能性があります。
  • 継続的に学習する: デザインパターンは、一度学べば終わりではありません。新しいパターンや、既存のパターンの新しい使用方法を継続的に学習することで、設計スキルを向上させます。

8. まとめ

GoFの23のデザインパターンは、ソフトウェア開発において重要な設計手法です。適切に活用することで、コードの再利用性、保守性、拡張性が向上し、より良いソフトウェアを効率的に開発できます。

  • 3つのカテゴリ: GoFの23のパターンは、生成、構造、振る舞いの3つのカテゴリに分類されます。各カテゴリは異なる設計上の問題を解決します。
  • 生成パターン: Singleton、Factory、Abstract Factory、Builder、Prototypeの5つのパターンが、オブジェクトの生成方法を抽象化します。
  • 構造パターン: Adapter、Bridge、Composite、Decorator、Facade、Flyweight、Proxyの7つのパターンが、クラスやオブジェクトの組み合わせを定義します。
  • 振る舞いパターン: Chain of Responsibility、Command、Interpreter、Iterator、Mediator、Memento、Observer、State、Strategy、Template Method、Visitorの11のパターンが、オブジェクト間の通信を定義します。
  • 適切な選択: 適切なパターンを選択し、実践的なプロジェクトで使用することで、より保守しやすく拡張可能なコードを書けるようになります。

まとめ

GoFの23のデザインパターンは、ソフトウェア開発において繰り返し発生する問題に対する再利用可能な解決策です。生成、構造、振る舞いの3つのカテゴリに分類され、それぞれ異なる設計上の問題を解決します。

生成パターンはオブジェクトの生成方法を抽象化し、構造パターンはクラスやオブジェクトの組み合わせを定義し、振る舞いパターンはオブジェクト間の通信を定義します。適切なパターンを選択し、実践的なプロジェクトで使用することで、より保守しやすく拡張可能なコードを書けるようになります。

ただし、過度な使用は避け、シンプルさを優先することが重要です。デザインパターンは問題を解決するための手段であり、目的ではありません。実践的なプロジェクトでデザインパターンを使用し、経験を積むことで、より良いソフトウェア設計ができるようになります。

Repositoryパターンとは?データアクセス層の設計 デザインパターン入門:なぜデザインパターンが重要なのか