はじめに
皆さん、こんにちは!「JavaとPythonで比べるデザインパターン」シリーズの第9回目です。
今回は、異なるインターフェースを持つクラス同士を協調させるためのAdapter(アダプター)パターンについて解説します。
Adapterパターンとは?
Adapterパターンは、互換性のないインターフェースを、クライアントが期待するインターフェースに変換するための構造パターンです。例えるなら、USB Type-Cポートしかない最新のノートパソコンに、古いUSB-Aデバイスを接続するための変換アダプターのようなものです。
Adapterパターンが必要な状況
このパターンは、以下の状況で特に威力を発揮します:
既存コードの再利用
既存のクラスを再利用したいが、そのインターフェースがシステムの要求と合わない場合。例えば、サードパーティライブラリを自社システムに組み込む際など。
インターフェース統一
異なるインターフェースを持つ複数のクラスを、一つの統一されたインターフェースで扱いたい場合。複数の外部APIを統一的に扱う場合などが典型例です。
段階的リファクタリング
レガシーシステムを新しいアーキテクチャに移行する際、既存コードを徐々に置き換えていくための橋渡し役として。
構成要素
Adapterパターンには以下の3つの主要な構成要素があります:
- Target(ターゲット): クライアントが期待するインターフェース
- Adaptee(アダプティー): 既存の互換性のないインターフェースを持つクラス
- Adapter(アダプター): TargetインターフェースとAdapteeの橋渡しをするクラス
Javaでの実装:厳格なインターフェース合わせ
Javaは厳格な型システムを持つため、異なるインターフェースを扱う際には、その違いを埋めるアダプタークラスを明示的に作成する必要があります。
より実践的な例として、古いログ出力システム(LegacyLogger
)を、新しいログ管理システム(ModernLogger
)のインターフェースで使用する場合を見てみましょう。
// JavaでのAdapterパターンの実装例
// 1. Target:クライアントが期待するインターフェース
interface ModernLogger {
void info(String message);
void error(String message, Exception e);
void debug(String message);
}
// 2. Adaptee:既存のログシステム(異なるインターフェース)
class LegacyLogger {
public void log(int level, String msg) {
String prefix = switch (level) {
case 1 -> "[INFO]";
case 2 -> "[ERROR]";
case 3 -> "[DEBUG]";
default -> "[UNKNOWN]";
};
System.out.println(prefix + " " + msg);
}
public void logError(String msg, String errorDetails) {
System.out.println("[ERROR] " + msg + " - Details: " + errorDetails);
}
}
// 3. Adapter:インターフェースを変換するクラス
class LegacyLoggerAdapter implements ModernLogger {
private final LegacyLogger legacyLogger;
public LegacyLoggerAdapter(LegacyLogger legacyLogger) {
this.legacyLogger = legacyLogger;
}
@Override
public void info(String message) {
// ModernLoggerのinfo()をLegacyLoggerのlog(1, message)に変換
legacyLogger.log(1, message);
}
@Override
public void error(String message, Exception e) {
// ModernLoggerのerror()をLegacyLoggerのlogError()に変換
legacyLogger.logError(message, e.getMessage());
}
@Override
public void debug(String message) {
// ModernLoggerのdebug()をLegacyLoggerのlog(3, message)に変換
legacyLogger.log(3, message);
}
}
// 使用例
public class LoggingClient {
private ModernLogger logger;
public LoggingClient(ModernLogger logger) {
this.logger = logger;
}
public void performOperation() {
logger.info("Operation started");
try {
// 何らかの処理
logger.debug("Processing data...");
} catch (Exception e) {
logger.error("Operation failed", e);
}
}
public static void main(String[] args) {
// 既存のレガシーシステム
LegacyLogger legacyLogger = new LegacyLogger();
// アダプターを通じて新しいインターフェースで使用
ModernLogger adaptedLogger = new LegacyLoggerAdapter(legacyLogger);
// クライアントは新しいインターフェースのみを知っている
LoggingClient client = new LoggingClient(adaptedLogger);
client.performOperation();
}
}
この例では、LegacyLoggerAdapter
がModernLogger
インターフェースを実装し、内部でLegacyLogger
のインスタンスを保持しています。これにより、クライアント(LoggingClient
)は既存のレガシーシステムの詳細を知る必要がなくなります。
Pythonでの実装:動的型付けとプロキシ
Pythonの動的型付けと「ダック・タイピング」により、Javaほど厳格なアダプターを必要としない場合もありますが、より明示的にアダプターの役割を定義することで、コードの意図を明確にできます。
基本的な実装
# PythonでのAdapterパターンの基本実装
# 1. Target:期待されるインターフェース
class ModernLogger:
def info(self, message):
raise NotImplementedError("Subclass must implement info method")
def error(self, message, exception=None):
raise NotImplementedError("Subclass must implement error method")
def debug(self, message):
raise NotImplementedError("Subclass must implement debug method")
# 2. Adaptee:既存のログシステム
class LegacyLogger:
def log(self, level, msg):
level_map = {1: "[INFO]", 2: "[ERROR]", 3: "[DEBUG]"}
prefix = level_map.get(level, "[UNKNOWN]")
print(f"{prefix} {msg}")
def log_error(self, msg, error_details):
print(f"[ERROR] {msg} - Details: {error_details}")
# 3. Adapter:インターフェースを変換
class LegacyLoggerAdapter(ModernLogger):
def __init__(self, legacy_logger):
self._legacy_logger = legacy_logger
def info(self, message):
self._legacy_logger.log(1, message)
def error(self, message, exception=None):
error_details = str(exception) if exception else "No details"
self._legacy_logger.log_error(message, error_details)
def debug(self, message):
self._legacy_logger.log(3, message)
# 使用例
class LoggingClient:
def __init__(self, logger):
self._logger = logger
def perform_operation(self):
self._logger.info("Operation started")
try:
# 何らかの処理
self._logger.debug("Processing data...")
# 例外を発生させてテスト
raise ValueError("Something went wrong")
except Exception as e:
self._logger.error("Operation failed", e)
# 実行例
if __name__ == "__main__":
# 既存のレガシーシステム
legacy_logger = LegacyLogger()
# アダプターを通じて新しいインターフェースで使用
adapted_logger = LegacyLoggerAdapter(legacy_logger)
# クライアントは新しいインターフェースのみを知っている
client = LoggingClient(adapted_logger)
client.perform_operation()
動的アダプター:__getattr__の活用
Pythonの特性を活かし、より柔軟なアダプターも作成できます:
# __getattr__を使った動的アダプター
class DynamicAdapter:
def __init__(self, adaptee, method_mapping=None):
self._adaptee = adaptee
self._method_mapping = method_mapping or {}
def __getattr__(self, name):
# メソッドマッピングをチェック
if name in self._method_mapping:
mapped_method = getattr(self._adaptee, self._method_mapping[name])
return mapped_method
# 直接的なメソッドマッピング
if hasattr(self._adaptee, name):
return getattr(self._adaptee, name)
# メソッドが見つからない場合
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
# 使用例
legacy_logger = LegacyLogger()
# メソッド名のマッピングを定義
method_mapping = {
'write_info': 'log', # write_info() -> log()
'write_debug': 'log' # write_debug() -> log()
}
adapter = DynamicAdapter(legacy_logger, method_mapping)
# 直接呼び出し可能
adapter.log(1, "Direct call")
# マッピング経由でも呼び出し可能
# adapter.write_info(1, "Mapped call") # これは実際にはlog()を呼ぶ
実践での活用例
API統合での活用
# 異なるAPIを統一インターフェースで扱う例
class PaymentProcessor:
def process_payment(self, amount, currency, card_info):
raise NotImplementedError
# 既存のPayPal API(異なるインターフェース)
class PayPalAPI:
def make_payment(self, dollars, credit_card):
print(f"PayPal: Processing ${dollars} with card {credit_card}")
return {"status": "success", "transaction_id": "pp_123"}
# 既存のStripe API(また別のインターフェース)
class StripeAPI:
def charge(self, cents, token):
print(f"Stripe: Charging {cents} cents with token {token}")
return {"id": "ch_456", "paid": True}
# PayPal用アダプター
class PayPalAdapter(PaymentProcessor):
def __init__(self, paypal_api):
self._paypal = paypal_api
def process_payment(self, amount, currency, card_info):
if currency != "USD":
raise ValueError("PayPal adapter only supports USD")
return self._paypal.make_payment(amount, card_info)
# Stripe用アダプター
class StripeAdapter(PaymentProcessor):
def __init__(self, stripe_api):
self._stripe = stripe_api
def process_payment(self, amount, currency, card_info):
if currency != "USD":
raise ValueError("Stripe adapter only supports USD")
cents = int(amount * 100) # ドルをセントに変換
return self._stripe.charge(cents, card_info)
# 統一的な使用
def process_payments(processors, amount, currency, card_info):
results = []
for processor in processors:
try:
result = processor.process_payment(amount, currency, card_info)
results.append(result)
except Exception as e:
results.append({"error": str(e)})
return results
# 使用例
paypal = PayPalAPI()
stripe = StripeAPI()
processors = [
PayPalAdapter(paypal),
StripeAdapter(stripe)
]
results = process_payments(processors, 100.0, "USD", "card_token_123")
print(results)
JavaとPythonの実装比較
特性 | Java | Python |
---|---|---|
型安全性 | コンパイル時に型チェック | 実行時に型エラーが発生する可能性 |
実装方法 | インターフェース実装による明示的変換 | クラス継承または動的メソッド委譲 |
柔軟性 | 厳密だが予測可能 | 動的で柔軟だが注意深い設計が必要 |
可読性 | インターフェースによる明確な契約 | シンプルだが意図が不明確になる場合も |
パフォーマンス | コンパイル時最適化 | 実行時のメソッド解決コスト |
まとめ:本質は「変換」にある
Adapterパターンは、両言語で実装のスタイルは大きく異なりますが、**「異なるインターフェースを統一的に扱う」**という本質は共通しています。
Javaのアプローチ
厳格な型システムを活かし、インターフェース実装による安全で予測可能な変換を行います。コンパイル時にエラーを検出できるため、大規模システムでの安全性が高くなります。
Pythonのアプローチ
動的型付けの特性を活かし、より柔軟で簡潔な実装が可能です。__getattr__
などの言語機能を使うことで、汎用的なアダプターも作成できますが、型安全性とのトレードオフを考慮する必要があります。
どちらのアプローチも、既存コードの再利用性を高め、システムの柔軟性を向上させるという共通の目標を達成します。選択する実装方法は、プロジェクトの要件や開発チームの特性に応じて決定しましょう。
次回予告: 「Day 10 Decoratorパターン:オブジェクトの振る舞いを動的に拡張する」
オブジェクトに新しい機能を動的に追加する方法と、JavaとPythonでの実装の違いを詳しく解説します。お楽しみに!