Help us understand the problem. What is going on with this article?

プロキシパターン(Proxy Pattern)をできる限りわかりやすく解説してみる

More than 1 year has passed since last update.

1. はじめに

この記事では、「動的プロキシ(DynamicProxy)をできる限りわかりやすく解説してみる」で説明する、動的プロキシの事前知識となるプロキシパターン(Proxy Pattern)を説明します。

本記事で掲載するJavaコードは、以下の環境で動作確認をしています。(Java以外の言語の動作環境は、随時記載します)また、掲載するJavaコードはコンパイルに必要な一部のimport句を省略しています。

名称 バージョン
OpenJDK 11.0.1

2. 「静的」なプロキシ

静的」なプロキシを解説します。これは、GoF(Gang of Four)のデザインパターンで紹介されている一般的なプロキシパターンに相当します。本章では静的なプロキシパターン(以下「プロキシ」と表記)の説明します。「動的」なプロキシについては、「動的プロキシ(DynamicProxy)をできる限りわかりやすく解説してみる」を参照してください。

2-1. 独自のインターフェースとクラスを定義する

プロキシを説明するための準備実装をします。MyInterfaceとその具象クラスであるMyClassを独自に定義します。これは、静的プロキシを適用するためのインターフェースとクラスです。

MyInterface.java
/** 独自のインターフェース定義 */
public interface MyInterface {
    public String myMethod(String value);
}
MyClass.java
/** MyInterfaceの実装クラス */
public class MyClass implements MyInterface {
    @Override
    public String myMethod(String value) {
        // MyClassのMyInterface.myMethodの処理
        System.out.println("Method:" + value);
        return "Result";
    }
}

上記のMyClassクラスをnewでオブジェクトを作成して、そのオブジェクトからmyMethodメソッドを呼び出す実装を、以下に示します。

実行コード
    MyInterface myObj = new MyClass();
    System.out.println(myObj.myMethod("Argument"));

上記を実行した結果、myMethodのメソッド内で出力したMethod:Argumentと、myMethodを呼び出した戻り値を出力したResultを標準出力に出力します。

実行結果
Method:Argument
Result

2-2. プロキシパターン

プロキシパターンとなる、静的なプロキシの説明します。

MyInterfaceの具象クラスであるMyClassオブジェクトの利用者は、MyClassのコンストラクタを呼び出してオブジェクトを生成し、そのオブジェクトのメソッドを呼び出していました。これは、プロキシを仲介しないオブジェクトの利用です。

実行コード(再掲)
    MyInterface myObj = new MyClass();
    System.out.println(myObj.myMethod("Argument"));

プロキシパターンでは、利用者とオブジェクトの仲介をする代理人の役割を果たします。代理人であるプロキシは、実際に利用するオブジェクトと同じインターフェースを持ちます。利用者はあたかも直接利用するオブジェクトのメソッドを直接呼出すように、プロキシのメソッドを呼び出します。プロキシはメソッドの呼び出しを受けて、事前処理を実行した後に実際に利用するオブジェクトの同じメソッドを呼び出して戻り値を受け取ります。プロキシは事後処理を実行した後に、利用者にプロキシから戻り値を返します。

下の絵は、プロキシのイメージを擬人化して表したものです。プロキシは日本語で代理人の意味となるので、図内では「代理人(プロキシ)」と表記しています。

proxy_image_small.png

現実の人と人との間でも、直接本人同士がやり取りをせずに代理人を仲介するメリットがあるように、プログラムの世界でもオブジェクトの呼出しにプロキシを仲介するメリットはあります。それは後述します。

2-3. 静的なプロキシの実装

それでは、具体的に実装をしていきましょう。

ここでは、プロキシパターンを実現する実装のMyProxyというクラスを新たに定義します。MyProxyクラスはMyClassと同じようにMyInterfaceの具象クラスです。MyProxyクラスは、MyInterfaceのオブジェクトをtargetObjという名前のクラスメンバを保持し、myMethodメソッドの中ではクラスメンバのtargetObjmyMethodを呼び出します。それに加えてプロキシ固有の処理となる、targetObjmyMethodを呼び出しの前後では、事前処理でMyProxy:start事後処理でMyProxy:endを標準出力に出力します。

MyProxy.java
/** MyInterfaceを実装するクラスの仲介をするクラス */
public class MyProxy implements MyInterface {
    /** 仲介先のオブジェクトを格納するクラス変数 */
    private final MyInterface targetObj;

    public MyProxy(MyInterface targetObj) {
        this.targetObj = targetObj;
    }

    @Override
    public String myMethod(String value) {
        // MyProxyの事前の処理
        System.out.println("Proxy:before");
        // MyClassオブジェクトのmyMethodメソッドを呼び出す
        String result = targetObj.myMethod(value);
        // MyProxyの事後の処理
        System.out.println("Proxy:after");
        // MyClassオブジェクトのmyMethodメソッドの戻り値を返却
        return result;
    }
}

上記で定義したMyProxyの使い方で最もシンプルな方法を紹介します、利用側の実装でMyProxyのオブジェクトをコンストラクタで作成します。

実行コード
    MyInterface myObj = new MyProxy(new MyClass());
    System.out.println(myObj.myMethod("Argument"));
実行結果
Proxy:before
Method:Argument
Proxy:after
Result

MyProxyの処理の実行を示すProxy:beforeProxy:afterの出力がMethod:Argumentの前後にあります。この実装例では、MyClassの変更なしでMyProxyの仲介でメソッドを呼び出すようになりました。プロキシパターンは、実装を一切変更せずに共通的あるいは付加的な処理を追加できる、という特徴があります。

2-4. プロキシの使用を強制する実装

上記の例では、オブジェクトのメソッドをプロキシを仲介して呼び出すか直接呼出すかは、利用側で任意に選択できました。しかし、あるクラスのオブジェクトは、必ずプロキシを経由して使用させたいケースがあるかもしれません。そのような場合にはプロキシの使用を強制して、不正な場合はコンパイルエラーで検出できるように実装できます。

これを実現するために、以下の方針でMyClassを修正します。

  1. コンストラクタをprivateにして、外部から直接コンストラクタ呼び出しによるオブジェクトの作成をできないようにする。
  2. MyClassのオブジェクトを取得するためのstaticのgetInstance()メソッドを定義する。MyClassのオブジェクトの利用者はgetInstance()メソッドからMyClassのオブジェクトを取得する。
  3. 2.のメソッドはMyClassのオブジェクトではなく、MyProxyのオブジェクトを返却する。

具体的なMyClassの実装を、以下に示します。

MyClass.java
/** MyInterfaceの実装クラス(MyProxy経由での利用を強制するパターン) */
public class MyClass implements MyInterface {
    /** 外部から直接MyClassオブジェクトを作成させいないため、デフォルトコンストラクタをprivateで定義する */
    private MyInterface() {}

    @Override
    public String myMethod(String value) {
        // MyClassのMyInterface.myMethodの処理
        System.out.println("Method:" + value);
        return "MyMethod result";
    }

    /** MyClassのインスタンスを取得するメソッド */
    public static MyInterface getInstance() {
        // MyClassのオブジェクトを作成
        MyClass myObj = new MyClass();
        // 上記のMyClassオブジェクトを持つMyProxyオブジェクトを作成
        MyProxy proxy = new MyProxy(myObj);
        // MyProxyオブジェクトを返却
        return proxy;
    }
}

上記のMyClassではnew MyClass()のように、直接コンストラクタを呼び出してオブジェクトを作成しようとすると、コンストラクタがprivateであるためにコンパイルエラーとなります。

このMyClassの実装はSingletoneパターンと似ていますが、getInstance()が毎回新しいオブジェクトを返却する点がSingletoneパターンとは異なります。もちろん、SingletoneパターンのようにgetInstanceが同じオブジェクトを返却する実装もできます。

MyClassの利用側の実装は以下になります。

実行コード
    MyInterface myObj = MyClass.getInstance();
    System.out.println(myObj.myMethod("Argument"));

実行結果は同じです。

実行結果
Proxy:before
Method:Argument
Proxy:after
Result

2-5. 静的なプロキシのまとめと活用例

ここで、これまでに実装したクラスのクラス図を示します。

staticproxy_class_small.png

改めて、静的なプロキシを適用する実装方針をまとめます。

  1. プロキシを通して呼び出したいクラスがある場合、そのクラスのインターフェースを定義する。
  2. プロキシのクラスを、1.で定義したインターフェースの具象クラスで定義する。
  3. 2.で定義したプロキシのクラスには、インターフェースのオブジェクトを格納するメンバ(クラス変数)を用意する。
  4. 2.で定義したプロキシのクラスのメソッドの中では、メンバの同じメソッドを呼び出す。(呼び出す前後でプロキシの処理を実装する)

これにより、オブジェクトの利用者がプロキシオブジェクトのメソッドを呼び出すと、プロキシオブジェクトの仲介で本当に呼び出したいオブジェクトのメソッドを呼び出します。

それでは、プロキシは具体的にどのような場面で活用できるのか、具体例を次に示します。

既存のクラスに処理を追加するプロキシ

変更不可能な既存のクラスの処理を追加するのは、プロキシパターンを有効に適用するケースの一つです。

例えば、Javaの標準ライブラリのクラスで古くから提供されているjava.text.SimpleDateFormatは、Date型とString型を相互に変換するクラスですが、同期化がされておらずひとつのSimpleDateFormatオブジェクトを複数のスレッドで同時に使うと不正な値を返却する可能性があります。

この問題の解決法の一つで、SimpleDateFormatのプロキシを定義して、プロキシ内で処理をsynchronizedブロックで同期化するか、スレッドごとに別のSimpleDateFormatオブジェクトを持つようにThreadLocalに格納します。SimpleDateFormatとスーパークラスのDateFormatは日付処理に関するインターフェースを持たないため、プロキシはSimpleDateFormatと共有するインターフェースを持ちません。

simpledateformat_proxy_class_small.png

MySimpleDateFormat.java
public class MySimpleDateFormat {
    private SimpleDateFormat innerFormat;

    public MySimpleDateFormat(String pattern) {
        innerFormat = new SimpleDateFormat(pattern);
    }

    public StringBuffer format(Date date,
                               StringBuffer toAppendTo,
                               FieldPosition fieldPosition) {
        synchronized(innerFormat) {
            innerFormat.format(date, toAppendTo, fieldPosition);
        }
    }

    /* その他のDateFormatのabstractメソッドも、上記と同様に実装する */
}

この方法は、二点問題があることを留意してください。

  • 現在のJava実行環境では性能面で最適ではないとされています。実行時にSimpleDateFormatのオブジェクトを都度生成する方が、性能面で有利である報告があります。
  • この方式は、直接SimpleDateFormatを使用する実装をコンパイルエラーで検知できません。システム開発内のコーディング規約等で禁止にするなどの運用ルールが必要です。

インターフェースに対する複数の具象クラスの共通処理を追加するプロキシ

あなたがひとつのインターフェースと、そのインターフェースで複数の具象クラスを実装したとします。
そのクラスは全て処理の前後で実行すべき共通の処理があるのであれば、プロキシを活用できます。

staticproxy_manyclass_small.png

これまでの実装例では、インターフェースに対して一つの具象クラスを定義して、そのクラスにプロキシを使用していました。本来は、共通のインターフェースを具象するクラスであれば複数の異なるクラスであっても、同じプロキシを共有して使用できます。プロキシは各クラスの前後で実行するべき共通処理を実装するクラスの役割を担います。

3. さいごに

本記事では、「静的な」プロキシの解説をしました。
この続きで、「動的プロキシ(DynamicProxy)をできる限りわかりやすく解説してみる」では動的プロキシを解説します。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした