はじめに
DIパターンとFactoryパターンの組み合わせは、柔軟でテスト可能なアプリケーションを構築する上で重要なデザインパターンです。この記事では、Pythonでの実装例を通じて、両パターンの効果的な使い方を解説します。
なぜDIパターンとFactoryパターンを組み合わせるのか?
解決したい課題
- オブジェクトの生成と使用の分離
- テスト容易性の向上
- 依存関係の明確化
- 実装の切り替えを容易にする
メリット
- 依存関係の制御が容易になる
- テストの書きやすさが向上する
- コードの再利用性が高まる
- 実装の差し替えが容易になる
デメリット
- 初期学習コストが高い
- 小規模なプロジェクトでは過剰な可能性がある
- ボイラープレートコードが増える
実装例
以下に、ログ出力システムを例にした実装を示します。
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)
実践的なユースケース
-
データベース接続の切り替え
- 開発環境とテスト環境で異なるDBを使用
- モックDBでのテスト実行
-
外部APIクライアントの差し替え
- 本番環境と開発環境での接続先切り替え
- テスト時のモック化
-
キャッシュ実装の切り替え
- メモリキャッシュとRedisの切り替え
- テスト時のモックキャッシュ使用
-
設定の動的ロード
- 環境変数からの設定読み込み
- 設定ファイルからの読み込み
まとめ
DIパターンとFactoryパターンの組み合わせは、以下のような場面で特に有効です:
- テスタビリティを重視する場合
- 実行時に実装を切り替えたい場合
- 依存関係を明確に管理したい場合
- マイクロサービスアーキテクチャでの実装
この組み合わせにより、保守性が高く、テストが容易なコードベースを実現できます。Pythonの型ヒントと抽象基底クラスを活用することで、より堅牢な実装が可能になります。