Java : Proxy パターン (図解/デザインパターン)
Proxy パターンとは、GoF によって定義されたデザインパターンの1つです。
操作対象のオブジェクトを直接操作はせずに、代理人をとおして操作する、という設計です。
本記事では、Proxy パターンを Java のコード付きで解説していきます。
デザインパターン(GoF) 関連記事
- 生成に関するパターン
- 振る舞いに関するパターン
概要
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) の設定だけを扱います。
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 クラス
に分割します。
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 になります。
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つのクラスにいろいろな役割を持たせすぎるのは良くないですからね。
補足
委譲
Proxy パターンに関連した手法として、委譲(delegation) があります。
転送(forwarding) と呼ばれることもあります。
Proxy パターンは、広い意味では委譲の一種と考えてよいのではないかな … と個人的には思います。
上記 Wikipedia を一読してみるのもよいかもですね。
継承との比較
利用例 のところで紹介した、
- Proxy パターンでキャッシュを実現
という例は、実は継承でも実現できます。
(注意:継承では実現できないような Proxy パターンもあります)
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 でログを出力
などがあります。
ぜひ有効に活用していきたいですね。
関連記事
- 標準APIにならう命名規則
- コメントが少なくて済むコードを目指そう
- シングルトン・パターンの乱用はやめよう
- メソッドのパラメータ(引数)は使う側でチェックしよう
- 不変オブジェクト(イミュータブル) とは
- 依存性の注入(DI)をもっと気軽に
- 不要になったコードはコメントアウトで残さずに削除しよう
- 簡易的な Builder パターン
- 読み取り専用(const) のインタフェースを作る
- 図解/デザインパターン一覧 (GoF)
- Abstract Factory パターン
- Adapter パターン
- Bridge パターン
- Builder パターン
- Chain of Responsibility パターン
- Command パターン
- Composite パターン
- Decorator パターン
- Facade パターン
- Factory Method パターン
- Flyweight パターン
- Interpreter パターン
- Iterator パターン
- Mediator パターン
- Memento パターン
- Observer パターン
- Prototype パターン
- Singleton パターン
- State パターン
- Strategy パターン
- Template Method パターン
- Visitor パターン