Javaにおける代理パターンの実装と応用

代理パターンは、オブジェクトへの直接アクセスが困難または不可能な場合に、代わりに中継する「代理」オブジェクトを介して間接的にアクセスを実現するための構造型デザインパターンです。クライアントが透過的に扱えるよう、代理オブジェクトと実オブジェクトは共通のインターフェースを実装する必要があります。

代理パターンは特定の目的に応じて多様な種類に分類され、それぞれが異なるシナリオに定位置を持ちます。代表的な種類には保護代理(Protect Proxy)、リモート代理(Remote Proxy)、仮想代理(Virtual Proxy)、キャッシュ代理(Cache Proxy)、スマート参照代理(Smart Reference Proxy)などがあります。

以下では、代理パターンの基本構造と実装方法、さらに実際の Java による具体例を通じてその適用方法を解説します。

代理パターンの基本構造

代理パターンの骨格は以下の3つのコンポーネントから構成されます。

(1) AbstractSubject(抽象テーマ)

代理と被代理の両方に対して共通の操作を宣言するインターフェースまたは抽象クラスです。クライアントはこの抽象層に依存します。

public interface Service {
    String execute(String input);
}

(2) RealSubject(実オブジェクト)

真正的なロジックを実装するクラスです。

public class RealService implements Service {
    @Override
    public String execute(String input) {
        // 高コスト処理(例: DBアクセス、ネットワーク通信など)
        return "Result for: " + input;
    }
}

(3) Proxy(代理オブジェクト)

RealSubject を保持し、操作の前後で追加の処理(権限チェック、ロギング、遅延初期化、キャッシュなど)を実行します。インターフェースは AbstractSubject を実装することで、クライアントからは透過的に扱われます。

public class ServiceProxy implements Service {
    private RealService realService;
    private boolean initialized = false;

    @Override
    public String execute(String input) {
        if (!initialized) {
            // 初回実行時だけ RealService を生成(仮想代理の一種)
            realService = new RealService();
            initialized = true;
        }
        
        // 前処理(例: 認証やログ出力などは単に省略しています)
        long startTime = System.currentTimeMillis();
        String result = realService.execute(input);
        long duration = System.currentTimeMillis() - startTime;
        System.out.println("処理時間: " + duration + " ms");
        
        return result;
    }
}

この例では、代理オブジェクトが遅延初期化(仮想代理)とパフォーマンス計測(スマート参照代理)を兼ねています。

具体的な応用例:業務検索システム

以下では、認証とログ記録を追加する形で、代理オブジェクトを使って既存ロジックを非変更で拡張するケースを紹介します。

構成要素

  • Authenticator:認証処理
  • AuditLogger:ログ出力
  • Searchable:抽象検索インターフェース
  • RealSearcher:実検索オブジェクト
  • SearchProxy:検索代理オブジェクト

実装

public class Authenticator {
    public boolean authenticate(String userId) {
        System.out.println("ユーザー '" + userId + "' の認証中...");
        boolean isValid = "taro".equals(userId);
        System.out.println(isValid ? "認証成功" : "認証失敗");
        return isValid;
    }
}

public class AuditLogger {
    public void logAccess(String userId, String keyword) {
        System.out.println("ユーザー '" + userId + "' がキーワード '" + keyword + "' で検索を使用");
    }
}
public interface Searchable {
    String search(String userId, String keyword);
}
public class RealSearcher implements Searchable {
    @Override
    public String search(String userId, String keyword) {
        System.out.println("検索処理: '" + keyword + "' (ユーザー: " + userId + ")");
        return "Result: XXXX件 found.";
    }
}
public class SearchProxy implements Searchable {
    private final Authenticator authenticator = new Authenticator();
    private final AuditLogger logger = new AuditLogger();
    private RealSearcher searcher;

    @Override
    public String search(String userId, String keyword) {
        if (!authenticator.authenticate(userId)) {
            return null;
        }
        if (searcher == null) {
            searcher = new RealSearcher(); // 初回アクセス時のみ初期化(仮想代理)
        }
        String result = searcher.search(userId, keyword);
        logger.logAccess(userId, keyword);
        return result;
    }
}

クライアントコード

public class Client {
    public static void main(String[] args) {
        Searchable proxy = new SearchProxy();

        System.out.println("--- 認証済みユーザのアクセス ---");
        String result1 = proxy.search("taro", "design pattern");
        System.out.println(result1);

        System.out.println("\n--- 認証失敗ユーザのアクセス ---");
        String result2 = proxy.search("hanako", "refactoring");
        System.out.println(result2 == null ? "アクセスが拒否されました。" : result2);
    }
}

出力例

--- 認証済みユーザのアクセス ---
ユーザー 'taro' の認証中...
認証成功
検索処理: 'design pattern' (ユーザー: taro)
Result: XXXX件 found.
ユーザー 'taro' がキーワード 'design pattern' で検索を使用

--- 認証失敗ユーザのアクセス ---
ユーザー 'hanako' の認証中...
認証失敗
アクセスが拒否されました。

このように、代理オブジェクトによって、既存の RealSearcher を一切変更せずに、認証・ロギングといった追加機能を横断的に適用できます。また、クライアントは Searchable インターフェースのみに依存するため、実オブジェクトか代理オブジェクトかを意識する必要がありません。

よく使われる代理の種類と特徴

種別 用途 利点
Remote Proxy リモートオブジェクト(RMI、Webサービス)に対応するローカルな代表者 ネットワーク通信の詳細を隠蔽し、クライアントはローカルオブジェクトと同様に扱える
Virtual Proxy 負荷の高いオブジェクトを遅延生成・効率的な代替で初期化 メモリ効率向上・起動時間短縮が可能
Cache Proxy 同一計算結果を再利用するためのキャッシュ層 同じリクエストの再処理を回避しレスポンス改善
Protect Proxy アクセス制御、権限確認 ユーザーやロールごとに粒度の異なるアクセス許可を実現
Smart Reference Proxy オブジェクト参照時の追加機能(例:参照回数記録) アクセスロギングや監視 USING に柔軟活用

効果と適用タイミング

代理パターンの導入によって得られる主な利益は、疎結合性の向上非機能要件の透過的追加です。")} 特に、以下のようなケースで効果が顕著です:

  • 実オブジェクトの生成やアクセスにコストがかかる(例:DBアクセス、网络操作)
  • 同じオブジェクトが複数箇所で頻繁に参照され、再利用による最適化が効く
  • アクセスに毎回同意層(認証、認可)のロジックが必要
  • サンドボックス的制限のもと、外部リソースへのインターフェースを提供したい

ただし、リモート通信型代理などのように、通信・同期のオーバーヘッドが増加する可能性があるため、用途に応じて慎重に選定することが重要です。

代理パターンはJava标准ライブラリでも広く利用されています。java.lang.reflect.Proxyjava.rmi.Remote を用いたリモートオブジェクト、Spring Framework の AOP 実装など、代理を基盤とする豪華なエコシステムが構築されています。

タグ: ProxyPattern DesignPattern Java RemoteProxy VirtualProxy

8月8日 04:30 投稿