広告

Java : Proxy パターン (図解/デザインパターン)

Proxy パターンとは、GoF によって定義されたデザインパターンの1つです。
操作対象のオブジェクトを直接操作はせずに、代理人をとおして操作する、という設計です。

本記事では、Proxy パターンを Java のコード付きで解説していきます。


デザインパターン(GoF) 関連記事

図解/デザインパターン一覧

概要

Proxy パターンは、プログラミングにおけるデザインパターンの一種。Proxy(プロキシ、代理人)とは、大まかに言えば、別物のインタフェースとして機能するクラスである。

Proxy パターンとは、

  • 操作対象のオブジェクトがあるけど 直接操作はしない
  • 代理人をとおして操作する

という設計です。

Proxy パターンの図

Proxy は、日本語的に発音すると「プロキシ」や「プロキシー」となります。
意味は「代理人」ですね。

Proxy パターンは、ずばりこういう用途に使える! というものではありません。
割とアイデア次第でいろいろなケースに使えます。

思いつくケースとしては、次のようなものがあります。

  • 重い処理を Proxy でキャッシュしてパフォーマンスを向上させる
  • 巨大なオブジェクトを Proxy 間で共有してメモリの節約
  • 各操作の前に Proxy でログを出力

などなど、他にもいろいろ使えそうです。

クラス図とコード例

クラス図

上の図は、Proxy パターンの一般的なクラス図です。

このクラス図をそのままコードにしてみます。

public interface Subject {
    void doAction();
}

public class RealSubject implements Subject {
    @Override
    public void doAction() {
        System.out.println("RealSubject : doAction!");
    }
}

public class Proxy implements Subject {
    private final RealSubject subject = new RealSubject();

    @Override
    public void doAction() {
        System.out.println("Proxy : doAction!");
        subject.doAction();
    }
}
public class Client {
    public void start() {

        final Subject subject = new Proxy();
        subject.doAction();

        // 結果
        // ↓
        //Proxy : doAction!
        //RealSubject : doAction!
    }
}

想定どおり、Client からは、Proxy クラスを経由して RealSubject の doAction メソッドが呼び出されました。

シーケンス図

しかし、これだけだと、なんのこっちゃという感じですよね…
もう少し具体的な例も見てみましょう。


利用例

Proxy パターンを使わない例

アプリの設定を読み書きする Config クラスを考えてみましょう。
今回はシンプルに、アプリで使う言語(Language) の設定だけを扱います。

クラス図2

public class Config {
    private final Path path = Path.of("R:", "java-work", "config.txt");

    public String getLang() throws IOException {
        return Files.readString(path);
    }

    public void setLang(String lang) throws IOException {
        Files.writeString(path, lang);
    }
}

メソッドは2つです。

  • setLang メソッド : アプリの言語を設定
  • getLang メソッド : アプリの言語を取得

設定した内容はファイルに保存します。

それでは、この Config クラスを使ってみましょう。
Client クラスで、設定された言語を取得します。

public class Client {
    public void start(Config config) throws IOException {
        System.out.println("--- 言語設定の取得 ---");

        final var lang = config.getLang();
        System.out.println(lang);
    }
}
final var config = new Config();
config.setLang("日本語");

final var client = new Client();
client.start(config);

// 結果
// ↓
//--- 言語設定の取得 ---
//日本語

Config.setLang で設定した言語が、getLang で取得できていますね。

さて、この Config クラスは問題なく動作しました。
しかし、ちょっとした不満もありした。

それは、Config.getLang のたびにファイルアクセスが発生してパフォーマンスが悪い、ということです。

public class Config {
    ...

    public String getLang() throws IOException {
        return Files.readString(path); // <---- 毎回ファイルアクセスが発生!
    }

    ...
}

Proxy パターンを使う例

それでは、Proxy パターンを使ってキャッシュ機能を実現してみましょう。

まずは、Config クラスを

  • Config インタフェース
  • ConfigImpl クラス

に分割します。

クラス図3

public interface Config {
    String getLang() throws IOException;

    void setLang(String lang) throws IOException;
}

public class ConfigImpl implements Config {
    private final Path path = Path.of("R:", "java-work", "config.txt");

    @Override
    public String getLang() throws IOException {
        System.out.println("ファイルアクセス!");
        return Files.readString(path);
    }

    @Override
    public void setLang(String lang) throws IOException {
        Files.writeString(path, lang);
    }
}

※ ConfigImpl.getLang メソッドに、ファイルアクセスしたことが分かるようにデバッグ用の出力を追加しています。

次は CacheConfig クラスです。
このクラスが Proxy になります。

クラス図4

public class CacheConfig implements Config {

    private final Config config = new ConfigImpl();
    private String lang;

    @Override
    public String getLang() throws IOException {
        if (lang == null) {
            lang = config.getLang();
        }

        return lang;
    }

    @Override
    public void setLang(String lang) throws IOException {
        this.lang = lang;
        config.setLang(lang);
    }
}

フィールドの lang 変数に、キャッシュとなる言語設定を保持します。
もし lang 変数が null であれば、実際の ConfigImpl を使ってファイルから読み込みます。

Client クラスでは、キャッシュされたことが分かるように、言語設定を複数回取得してみましょう。

public class Client {
    public void start(Config config) throws IOException {
        System.out.println("--- 言語設定の取得 ---");

        for (int i = 0; i < 5; i++) {
            final var lang = config.getLang();
            System.out.printf("i = %d : lang = %s%n", i, lang);
        }
    }
}
final var config = new CacheConfig();

final var client = new Client();
client.start(config);

// 結果
// ↓
//--- 言語設定の取得 ---
//ファイルアクセス!
//i = 0 : lang = 日本語
//i = 1 : lang = 日本語
//i = 2 : lang = 日本語
//i = 3 : lang = 日本語
//i = 4 : lang = 日本語

これで、getLang メソッドを複数回呼び出しても、ファイルアクセスは最初の1回だけになりました。

クラスの役割としても、

  • ConfigImpl : 設定ファイルの読み書き
  • CacheConfig : キャッシュ機能

と分けられたので、良い感じです。
1つのクラスにいろいろな役割を持たせすぎるのは良くないですからね。


補足

委譲

委譲 (英: delegation) とはオブジェクト指向プログラミングにおいて、あるオブジェクトの操作を一部他のオブジェクトに代替させる手法のこと。

Proxy パターンに関連した手法として、委譲(delegation) があります。
転送(forwarding) と呼ばれることもあります。

Proxy パターンは、広い意味では委譲の一種と考えてよいのではないかな … と個人的には思います。
上記 Wikipedia を一読してみるのもよいかもですね。

継承との比較

利用例 のところで紹介した、

  • Proxy パターンでキャッシュを実現

という例は、実は継承でも実現できます。
(注意:継承では実現できないような Proxy パターンもあります)

クラス図5

public class Config {
    private final Path path = Path.of("R:", "java-work", "config.txt");

    public String getLang() throws IOException {
        return Files.readString(path);
    }

    public void setLang(String lang) throws IOException {
        Files.writeString(path, lang);
    }
}

public class CacheConfig extends Config {

    private String lang;

    @Override
    public String getLang() throws IOException {
        if (lang == null) {
            lang = super.getLang();
        }

        return lang;
    }

    @Override
    public void setLang(String lang) throws IOException {
        this.lang = lang;
        super.setLang(lang);
    }
}

上記は、継承でキャッシュ機能を実現したコード例になります。

さて、

  • Proxy パターンのような 委譲 を使う
  • 継承を使う

どちらがよいのでしょうか? それはなかなかに難しい問題です。
ケースによっても変わると思います。

一般的には、継承より 委譲 のほうがクラス間の依存は弱くなります。
これは委譲を使うメリットの1つでしょう。

例えば、ユニットテストをする場合は、委譲のほうがやりやすいことが多いと思います。
(モックやダミークラスが作りやすいため)

継承か委譲かで悩む場合 … つまり、どちらでもよいようなケースでは、委譲を使うことをおすすめします。

まとめ

Proxy パターンとは、

  • 操作対象のオブジェクトがあるけど直接操作はしない
  • 代理人をとおして操作する

という設計です。

使えるケースとしては、

  • 重い処理を Proxy でキャッシュしてパフォーマンスを向上させる
  • 巨大なオブジェクトを Proxy 間で共有してメモリの節約
  • 各操作の前に Proxy でログを出力

などがあります。

ぜひ有効に活用していきたいですね。


関連記事

ページの先頭へ