1
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?

デザインパターン Proxyパターン

Last updated at Posted at 2023-11-18

Proxyパターン

特定のオブジェクトに対するアクセスや操作を、Proxy(代理)オブジェクトを介して行う方法。

Decorator パターンに似ている。

Decorator パターンが、既存のオブジェクトに対して透過的に新たな振る舞い、責務を追加することを目的としているのに対して、Proxy パターンはアクセス制御を主な目的としている

また、Decorartor パターンではオブジェクトが何重にもラップされることがあるのに対して、Proxy パターンでは通常オブジェクトが何重にも重ねてラップされることはない。

ただし、Proxy パターンは、アクセス制御以外にも様々な目的で利用できる。

利用場面

アクセス制御・保護プロキシ

アクセス制御や権限管理に利用される。
Proxy を介入させることで、特定のユーザーに対してのみアクセスを許可し、他のユーザーからのアクセスに制限をかけることができる。

リモートアクセス制御・リモートプロキシ

Proxy が(リモート)サーバー上に存在するオブジェクトにアクセスするための手段として利用される。
クライアント側からは、クライアントと同じローカル環境に存在するオブジェクトを操作しているように見えるが、実際にはネットワーク越しのリモート環境のオブジェクトに対する操作が実行される。

遅延初期化・仮想プロキシ

リソースの遅延初期化に利用される。
特定のオブジェクトの生成や初期化に時間がかかる場合、それらの処理をプロキシに行わせることでプログラムの効率化を図ることができる。

キャッシング

仮想プロキシの特殊な形式。
計算コストの高い操作結果を一時的に保存し、同じ操作が要求された時に結果をキャッシュから提供することで、ネットワークによる処置の遅延を軽減させることができる。
また、複数のクライアントが結果を共有することもできる。
また、複数のスレッドからのアクセスを安全に行うためのアクセス制御にも利用できる。

モック

Proxy がテスト時に本番用のオブジェクトの代わりに使用できるモックオブジェクトとなる。

Proxyパターンの構成要素

Subject

主体、主題。
Proxy と RealSubject 共通のインターフェース(または抽象クラス)。

Proxy

RealSubject の代理。
RealSubject をラップすることで、クライアントからの要求を中継、または制御する。
RealSubject への参照を保持する(コンポジション)。

Real Subject

実際の処理を実装したオブジェクト。
Proxy によってラップされるため、クライアントから直接利用されなくなり、Proxy を介して利用されるようになる。

Proxy.png

保護プロキシの例

RealSubject が持つ request1()request2() を使用不可にしたい場合、Proxy を利用することでアクセスを制御することができる。

Subject.java
public interface Subject {
    public void request1();
    public void request2();
    public void request3();
}
RealSubject.java
public class RealSubject implements Subject {

    @Override
    public void request1() {
        System.out.println("request1 を実行します。");
    }

    @Override
    public void request2() {
        System.out.println("request2 を実行します。");
    }

    @Override
    public void request3() {
        System.out.println("request3 を実行します。");
    }
}
Proxy.java
public class Proxy implements Subject {
    // Real Subject を保持
    private Subject realSubject = new RealSubject();

    @Override
    public void request1() {
        // Proxy によって、使用不可のメソッドにできる(例外を発生させる方法もある)
        System.out.println("権限がありません。");
    }

    @Override
    public void request2() {
        // Real Subject に処理を転送する
        realSubject.request2();
    }

    @Override
    public void request3() {
        // Proxy によって、使用不可のメソッドにできる(例外を発生させる方法もある)
        System.out.println("権限がありません。");
    }
}
Main.java
public class Main {
    public static void main(String[] args) {

        // Real Subject を使用した時
        Subject realSubject = new RealSubject();
        realSubject.request1();
        realSubject.request2();
        realSubject.request3();
        // >> request1 を実行します。
        // >> request2 を実行します。
        // >> request3 を実行します。


        // Proxy を利用した時
        Subject proxy = new Proxy();
        proxy.request1();
        proxy.request2();
        proxy.request3();
        // >> 権限がありません。
        // >> request2 を実行します。
        // >> 権限がありません。
    }
}

遅延初期化の例

遅延初期化は、Proxy の生成時に RealSubject を生成するのではなく、request1()request2()request3()のいずれかのリクエストが要求されたタイミングで生成を行う。

RealSubjectの生成が非常に負荷のかかる処理であるために、アプリケーション起動時などに初期化処理を実行させたくない場合や、RealSubjectが非常にサイズの大きなオブジェクトであるために、メモリを節約したい場合などに有効。

また、循環参照・循環依存(あるオブジェクトAが別のオブジェクトBに依存し、一方で、オブジェクトBもオブジェクトAに依存しているような状況)が発生している状況で、オブジェクトの初期化の順番の管理が複雑になってしまった場合にも有効。

Proxy.java
public class Proxy implements Subject {

    private Subject realSubject;

    private Subject getRealSubject() {
        if (realSubject == null) {
            // Real Subject は初回リクエストを受けつけたタイミングで初めて生成される
            realSubject = new RealSubject();
        }

        return realSubject;
    }

    @Override
    public void request1() {
        getRealSubject().request1();
    }

    @Override
    public void request2() {
        getRealSubject().request2();
    }

    @Override
    public void request3() {
        getRealSubject().request3();
    }
}

キャッシングの例

RealSubject が、通信を利用した重たい処理によって何らかのデータを取得しているような場合に、同じデータに対して複数回取得リクエストが行われたときに、2度目以降のリクエストでは通信を使用せず、Proxy が保持するキャッシュを利用させることができる。

Subject.java
public interface Subject {
    public String fetchData(String key);
}
RealSubject.java
public class RealSubject implements Subject {
    @Override
    public String fetchData(String key) {
        // 実際のデータを取得する処理(通信を利用した重たい処理)
        System.out.println("通信中...");
        System.out.println(key + "を使用して、データを取得します。。。");
        return key + "をもとに取得したデータ";
    }
}
Proxy.java
import java.util.HashMap;
import java.util.Map;

public class Proxy implements Subject {

    private Subject realSubject;

    // データをキャッシュしながら保持するためのマップ
    private Map<String, String> cache = new HashMap<>();

    // コンストラクタが Real Subject を受け取る
    public Proxy(Subject realSubject) {
        this.realSubject = realSubject;
    }

    @Override
    public String fetchData(String key) {
        // キャッシュにデータが存在する場合、キャッシュからデータを取得
        if (cache.containsKey(key)) {
            System.out.println("キャッシュから" + key + "に対応するデータを取得します。");
            return cache.get(key);
        } else {
            // キャッシュにデータが存在しない場合、RealSubject からデータを取得
            String data = realSubject.fetchData(key);
            // 取得したデータをキャッシュに保存
            cache.put(key, data);
            return data;
        }
    }
}
Main.java
public class Main {
    public static void main(String[] args) {
        Subject realSubject = new RealSubject();
        Subject proxy = new Proxy(realSubject);

        System.out.println(proxy.fetchData("key1"));
        System.out.println(proxy.fetchData("key2"));
        System.out.println(proxy.fetchData("key1"));
        System.out.println(proxy.fetchData("key3"));
        System.out.println(proxy.fetchData("key2"));

        // >> 通信中...
        // >> key1を使用して、データを取得します。。。
        // >> key1をもとに取得したデータ

        // >> 通信中...
        // >> key2を使用して、データを取得します。。。
        // >> key2をもとに取得したデータ

        // >> キャッシュからkey1に対応するデータを取得します。
        // >> key1をもとに取得したデータ

        // >> 通信中...
        // >> key3を使用して、データを取得します。。。
        // >> key3をもとに取得したデータ

        // >> キャッシュからkey2に対応するデータを取得します。
        // >> key2をもとに取得したデータ
    }
}

動的Proxy

Proxy クラスが実行時に作成されるProxy パターン。

通常の静的な Proxy よりも、さらに柔軟性、拡張性が求められる場面で利用される。

Java では java.lang.reflect パッケージで動的プロキシがサポートされている。Proxy クラスはjava.lang.reflectクラスによってプログラム実行時に作成されるため、開発者は Proxy クラスを実装する必要がない。

リフレクション

リフレクションはプログラムの実行時にクラス(型情報)やメソッドの情報にアクセスするための仕組みであり、動的Proxy を実現する上で重要な役割を果たしている。

Java はコンパイル型言語であるため、通常、型情報の解析はコンパイル時に行われる。リフレクションを使用した場合、型情報の解析が実行時に行われるため、通常よりもコストがかかる。

さらに、リフレクションを使用すると、非公開(プライベート)メソッドへのアクセスも可能なため、セキュリティ上のリスクがあることも理解しながら使用する必要がある。

動的 Proxy パターンの構成要素

Subject

Proxy と RealSubject 共通のインターフェース(または抽象クラス)。

Real Subject

実際の処理を実装したオブジェクト。

Proxy Class

Dynamic Proxy を生成するためのクラス。
Java ではjava.lang.reflect.Proxyクラスが Proxy Class の機能を持つ。
Dynamic Proxy オブジェクトを、静的メソッド Proxy.newProxyInstance() によってプログラム実行時に生成する。

public static Object newProxyInstance(
ClassLoader loader,   → クラスローダー
Class>[] interfaces,   → インターフェース( Subject
InvocationHandler h   → 呼び出しハンドラ( InvocationHandler
) throws IllegalArgumentException

Dynamic Proxy

開発者は実装しないクラス。
実行時に動的に生成される Real Subject の代理オブジェクト。
Dynamic Proxy は、メソッド実行のリクエストを受け取ると、Invocation Handler にそのリクエストを転送する。

Invocation Handler

呼び出しハンドラ。メソッドの呼び出しに対応する役割を持つ。
Real Subject への参照を保持している(コンポジション)。
Proxy と同様に、java.lang.reflectパッケージに含まれる。
Invocation Handler は、Dynamic Proxy から転送されたリクエストを処理する。
処理、と言うのは、メソッド名、引数などの情報をもとに、実行すべき処理を判断する。
メソッド名で判断する場合は「get」から始まるメソッドが呼ばれたとか、特定の文字列を含むメソッド名が呼ばれた、など。
引数で判断する場合は、引数の数や、引数の型など。

Invocation Handler が実質的に Proxy の役割を果たす

Dynamic_Proxy.png

一連の流れ

Dynamic_Proxy_1.png

Dynamic_Proxy_2.png

Dynamic_Proxy_3.png

Dynamic_Proxy_4.png

動的Proxy の例

Subject.java
public interface Subject {
    public void request1();
    public void request2(String arg);
}
RealSubject.java
public class RealSubject implements Subject {

    @Override
    public void request1() {
        System.out.println("request1 を実行します。");
    }

    @Override
    public void request2(String arg) {
        System.out.println("request2 を実行します。 引数:" + arg);
    }
}
MyInvocationHandler.java
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class MyInvocationHandler implements InvocationHandler {

    private Subject realSubject;

    public MyInvocationHandler(Subject realSubject) {
        this.realSubject = realSubject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
        
            // メソッド名で判断する
            if (method.getName().contains("1")) {
                System.out.println("ここで代理処理を実行する");
                return method.invoke(realSubject, args);
                
            // 引数の型で判断する
            } else if (args[0] instanceof String) {
                System.out.println("ここで代理処理を実行する");
                return method.invoke(realSubject, args);
                
            }
        } catch (Exception e) {
            // Real Subject が例外を発生させた時の処理
            
        }

        // Real Subject の処理は実行されず、クライアントには null が返却される
        return null;
    }
}
Main.java
import java.lang.reflect.Proxy;

public class Main {

    public static void main(String[] args) {

        // Real Subject
        Subject realSubject = new RealSubject();
        
        // 動的Proxy を生成する
        Subject proxy = (Subject) Proxy.newProxyInstance(
                realSubject.getClass().getClassLoader(),
                realSubject.getClass().getInterfaces(),
                new MyInvocationHandler(realSubject)
        );

        proxy.request1();
        // > ここで代理処理を実行する
        // > request1 を実行します。

        proxy.request2("val2");
        // > ここで代理処理を実行する
        // > request2 を実行します。 引数:val2
    }
}

参考

Head Firstデザインパターン ―頭とからだで覚えるデザインパターンの基本

1
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
1
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?