2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DIパターン × Factoryパターン 〜柔軟なオブジェクト生成の実装

Posted at

はじめに

DIパターンとFactoryパターンの組み合わせは、柔軟でテスト可能なアプリケーションを構築する上で重要なデザインパターンです。この記事では、Pythonでの実装例を通じて、両パターンの効果的な使い方を解説します。

image.png

なぜDIパターンとFactoryパターンを組み合わせるのか?

解決したい課題

  1. オブジェクトの生成と使用の分離
  2. テスト容易性の向上
  3. 依存関係の明確化
  4. 実装の切り替えを容易にする

メリット

  • 依存関係の制御が容易になる
  • テストの書きやすさが向上する
  • コードの再利用性が高まる
  • 実装の差し替えが容易になる

デメリット

  • 初期学習コストが高い
  • 小規模なプロジェクトでは過剰な可能性がある
  • ボイラープレートコードが増える

実装例

以下に、ログ出力システムを例にした実装を示します。

from abc import ABC, abstractmethod
from typing import List

# インターフェース定義
class Logger(ABC):
    @abstractmethod
    def log(self, message: str) -> None:
        pass

class LoggerFactory(ABC):
    @abstractmethod
    def create_logger(self) -> Logger:
        pass

# 具体的な実装
class ConsoleLogger(Logger):
    def log(self, message: str) -> None:
        print(f"[Console] {message}")

class FileLogger(Logger):
    def log(self, message: str) -> None:
        print(f"[File] {message}")
        # 実際のファイル出力処理は省略

# Factory実装
class ConsoleLoggerFactory(LoggerFactory):
    def create_logger(self) -> Logger:
        return ConsoleLogger()

class FileLoggerFactory(LoggerFactory):
    def create_logger(self) -> Logger:
        return FileLogger()

# サービスクラス
class UserService:
    def __init__(self, logger_factory: LoggerFactory):
        self.logger = logger_factory.create_logger()

    def create_user(self, username: str) -> None:
        # ユーザー作成のロジック
        self.logger.log(f"Created user: {username}")

# 使用例
def main():
    # コンソール出力を使用する場合
    console_logger_factory = ConsoleLoggerFactory()
    user_service_with_console = UserService(console_logger_factory)
    user_service_with_console.create_user("John")

    # ファイル出力を使用する場合
    file_logger_factory = FileLoggerFactory()
    user_service_with_file = UserService(file_logger_factory)
    user_service_with_file.create_user("Alice")

def run_examples():
    print("\n=== 基本的な使用例 ===")
    main()

    print("\n=== 環境に応じたロガーの切り替え ===")
    dev_service = configure_service(Environment.DEVELOPMENT)
    dev_service.create_user("DevUser")
    
    staging_service = configure_service(Environment.STAGING)
    staging_service.create_user("StagingUser")

    print("\n=== 複数ロガーの組み合わせ ===")
    composite_factory = setup_composite_logging()
    composite_service = UserService(composite_factory)
    composite_service.create_user("CompositeUser")

if __name__ == "__main__":
    run_examples()

"""
実行結果:
=== 基本的な使用例 ===
[Console] Created user: John
[File] Created user: Alice

=== 環境に応じたロガーの切り替え ===
[Console] Created user: DevUser
[File] Created user: StagingUser

=== 複数ロガーの組み合わせ ===
[Console] Created user: CompositeUser
[File] Created user: CompositeUser
"""

テストの例

import unittest
from typing import List

class MockLogger(Logger):
    def __init__(self):
        self.logs: List[str] = []

    def log(self, message: str) -> None:
        self.logs.append(message)

class MockLoggerFactory(LoggerFactory):
    def __init__(self):
        self.mock_logger = MockLogger()

    def create_logger(self) -> Logger:
        return self.mock_logger

    def get_mock_logger(self) -> MockLogger:
        return self.mock_logger

class TestUserService(unittest.TestCase):
    def test_create_user(self):
        # テストのセットアップ
        mock_factory = MockLoggerFactory()
        user_service = UserService(mock_factory)
        mock_logger = mock_factory.get_mock_logger()

        # テスト実行
        user_service.create_user("TestUser")

        # 検証
        self.assertEqual(mock_logger.logs[0], "Created user: TestUser")

if __name__ == '__main__':
    unittest.main()

実践的な拡張例

1. 環境に応じたロガーの切り替え

from enum import Enum

class Environment(Enum):
    DEVELOPMENT = "development"
    STAGING = "staging"
    PRODUCTION = "production"

class LoggerFactoryProvider:
    @staticmethod
    def get_factory(env: Environment) -> LoggerFactory:
        if env == Environment.DEVELOPMENT:
            return ConsoleLoggerFactory()
        elif env == Environment.STAGING:
            return FileLoggerFactory()
        else:
            return CloudLoggerFactory()  # 本番環境用のロガー

# 使用例
def configure_service(env: Environment) -> UserService:
    factory = LoggerFactoryProvider.get_factory(env)
    return UserService(factory)

2. 複数のロガーを組み合わせる

class CompositeLogger(Logger):
    def __init__(self, loggers: List[Logger]):
        self.loggers = loggers

    def log(self, message: str) -> None:
        for logger in self.loggers:
            logger.log(message)

class CompositeLoggerFactory(LoggerFactory):
    def __init__(self, factories: List[LoggerFactory]):
        self.factories = factories

    def create_logger(self) -> Logger:
        loggers = [factory.create_logger() for factory in self.factories]
        return CompositeLogger(loggers)

# 使用例
def setup_composite_logging():
    factories = [
        ConsoleLoggerFactory(),
        FileLoggerFactory()
    ]
    return CompositeLoggerFactory(factories)

実践的なユースケース

  1. データベース接続の切り替え

    • 開発環境とテスト環境で異なるDBを使用
    • モックDBでのテスト実行
  2. 外部APIクライアントの差し替え

    • 本番環境と開発環境での接続先切り替え
    • テスト時のモック化
  3. キャッシュ実装の切り替え

    • メモリキャッシュとRedisの切り替え
    • テスト時のモックキャッシュ使用
  4. 設定の動的ロード

    • 環境変数からの設定読み込み
    • 設定ファイルからの読み込み

まとめ

image.png

DIパターンとFactoryパターンの組み合わせは、以下のような場面で特に有効です:

  • テスタビリティを重視する場合
  • 実行時に実装を切り替えたい場合
  • 依存関係を明確に管理したい場合
  • マイクロサービスアーキテクチャでの実装

この組み合わせにより、保守性が高く、テストが容易なコードベースを実現できます。Pythonの型ヒントと抽象基底クラスを活用することで、より堅牢な実装が可能になります。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?