0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

全30回:静的と動的でどう違うのか、JavaとPythonで学ぶデザインパターン - Day 9 Adapterパターン:異なるインターフェースを橋渡しする

Posted at

はじめに

皆さん、こんにちは!「JavaとPythonで比べるデザインパターン」シリーズの第9回目です。
今回は、異なるインターフェースを持つクラス同士を協調させるためのAdapter(アダプター)パターンについて解説します。

Adapterパターンとは?

Adapterパターンは、互換性のないインターフェースを、クライアントが期待するインターフェースに変換するための構造パターンです。例えるなら、USB Type-Cポートしかない最新のノートパソコンに、古いUSB-Aデバイスを接続するための変換アダプターのようなものです。

Adapterパターンが必要な状況

このパターンは、以下の状況で特に威力を発揮します:

既存コードの再利用
既存のクラスを再利用したいが、そのインターフェースがシステムの要求と合わない場合。例えば、サードパーティライブラリを自社システムに組み込む際など。

インターフェース統一
異なるインターフェースを持つ複数のクラスを、一つの統一されたインターフェースで扱いたい場合。複数の外部APIを統一的に扱う場合などが典型例です。

段階的リファクタリング
レガシーシステムを新しいアーキテクチャに移行する際、既存コードを徐々に置き換えていくための橋渡し役として。

構成要素

Adapterパターンには以下の3つの主要な構成要素があります:

  1. Target(ターゲット): クライアントが期待するインターフェース
  2. Adaptee(アダプティー): 既存の互換性のないインターフェースを持つクラス
  3. 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();
    }
}

この例では、LegacyLoggerAdapterModernLoggerインターフェースを実装し、内部で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での実装の違いを詳しく解説します。お楽しみに!

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?