Java : Prototype パターン (図解/デザインパターン)
Prototype パターンとは、GoF によって定義されたデザインパターンの1つです。
自身の属性を引き継いだ新しいオブジェクトを生成します。
本記事では、Prototype パターンを Java のコード付きで解説していきます。
デザインパターン(GoF) 関連記事
- 生成に関するパターン
- 振る舞いに関するパターン
概要
Prototype パターンとは、
- 自身の属性を引き継いだ新しいオブジェクトを生成する
という設計です。
Prototype は、日本語的に発音すると「プロトタイプ」となります。
意味は「原型」または「試作品」ですね。
この記事では、属性を引き継いだオブジェクトを生成することを「複製」と表記します。
【Prototype パターンのイメージ】
ある1つのオブジェクトがあるとします。
そのオブジェクトは、いくつかの属性を持ちます。
ただし、外側からはそれらの属性にアクセスできません。
属性を取得するメソッドもありません。
例えば次のようなクラスです。
public class A {
private int n1;
private int n2;
public void increment() {
n1 += 1;
n2 += 2;
}
public void print() {
System.out.println("n1 = " + n1 + " : n2 = " + n2);
}
}
さて、このオブジェクトを複製したいとしましょう。
外部から属性にアクセスできないので、どうしたものか…
やりたいことの本質は、
- クラスの詳細(属性) を知らなくてもオブジェクトを複製したい
です。
その答えの1つが Prototype パターンです。
Prototype パターンでは、複製したいオブジェクトに
- clone (複製)
メソッドを持たせます。
外部に公開していない属性であっても、自分自身 であればアクセスできます。
そのため、属性が同じオブジェクトでも作れるわけですね。
もう1つのケースを考えてみましょう。
クライアントにはインタフェースだけが 公開 されていて、その実装クラスは 非公開 です。
さて、クライアントは、このオブジェクトを複製したいとしましょう。
今度は、外部から属性にアクセスできないだけではなく、
- 実装クラスを new できない
という問題もあります。
(実装クラスA、実装クラスBが非公開のため)
やりたいことの本質は先ほどと同じで、
- クラスの詳細(実装クラス) を知らなくてもオブジェクトを複製したい
です。
このケースも Prototype パターンが使えます。
公開されているインタフェースに
- clone (複製)
メソッドを宣言すれば OK ですね。
実際の処理では、実装クラスA、実装クラスBで、それぞれオブジェクトが複製されます。
関連記事:
【補足】
Java をある程度使ったことのあるかたは、
- Object クラスの clone メソッド
をご存じかもしれません。
その名のとおり、オブジェクトを複製するメソッドですね。
Object クラスの clone メソッドについては 後ほど 軽く解説します。
クラス図とシーケンス図
上の図は、Prototype パターンの一般的なクラス図とシーケンス図です。
それでは、このクラス図をそのままコードにしてみましょう。
【注意点】
本来であれば、Prototype インタフェースのメソッド名は clone にしたいところです。
しかし、Java では clone メソッドは、すでに Object クラスで使われています。
よって、本記事の例では仕方なく Prototype インタフェースのメソッド名は cloneObj としています。
これは、純粋な Prototype パターンの例を見せたいためです。
Java 以外の人には、Object クラスとか関係ないですからね …
少し分かりにくいかもしれませんが、ご了承ください。
まずは Prototype インタフェースですね。
先述したとおり、メソッド名は clone の代わりに cloneObj としています。
public interface Prototype {
Prototype cloneObj();
}
次は、その実装クラスです。
属性として、フィールド(private) に attr を持たせています。
ただし、getAttr メソッドのような、直接 attr を取得する手段はありません。
public class ConcretePrototype implements Prototype {
private String attr;
public void setAttr(String attr) {
this.attr = attr;
}
@Override
public Prototype cloneObj() {
final var cloned = new ConcretePrototype();
cloned.setAttr(attr);
return cloned;
}
@Override
public String toString() {
return "attr = " + attr;
}
}
それでは、これらのクラスを使ってみましょう。
final var prototype = new ConcretePrototype();
prototype.setAttr("abc");
System.out.println(prototype); // attr = abc
final var cloned1 = prototype.cloneObj();
System.out.println(cloned1); // attr = abc
prototype.setAttr("XYZ");
System.out.println(prototype); // attr = XYZ
final var cloned2 = prototype.cloneObj();
System.out.println(cloned2); // attr = XYZ
結果も問題なしですね。
無事に、同じ属性のオブジェクトが複製できました。
具体的な例
Prototype パターンのイメージ のところで解説したケースとは、ちょっと違う例も見てみましょう。
次のような GUI を考えてみます。
とはいえ、GUI をコード例にするのは複雑になるので、ここでは単純なテキスト出力のみで考えます。
(あくまで上記のダイアログはイメージということで…)
ダイアログは、UI の部品として
- ラベル : 「処理を実行します。」
- ボタン : 「OK」
を持ちます。
(ラベルとボタンの文字列は変更可能とします)
通常のアプリであれば、いろいろなダイアログが必要となるでしょう。
そのたびに、
- ラベルを生成
- ボタンを生成
- 上記の部品を使ってダイアログを生成
とするのは手間がかかります。
そこで、Prototype パターンを使います。
ある程度ベースとなるダイアログを作っておいて、それを複製することで生成の手間を減らします。
それでは、コードを書いていきましょう。
まずは Prototype インタフェースです。
先述したとおり、メソッド名は clone の代わりに cloneObj としています。
public interface Prototype {
Prototype cloneObj();
}
次は Label と Button クラスです。
自身を複製する cloneObj メソッドを実装します。
(本来なら、Button クラスにはボタンを押したときの action メソッドがあったほうがよいでしょう。
ただ、今回はコード例を簡単にするために省きます)
public class Label implements Prototype {
private String text;
public void setText(String text) {
this.text = text;
}
@Override
public Label cloneObj() {
final var cloned = new Label();
cloned.setText(text);
return cloned;
}
@Override
public String toString() {
return "ラベル : " + text;
}
}
public class Button implements Prototype {
private String text;
public void setText(String text) {
this.text = text;
}
@Override
public Button cloneObj() {
final var cloned = new Button();
cloned.setText(text);
return cloned;
}
@Override
public String toString() {
return "ボタン : " + text;
}
}
Dialog クラスは、Label と Button オブジェクトをフィールドに持ちます。
ポイントは cloneObj メソッドです。
Label と Button オブジェクトも複製します。
public class Dialog implements Prototype {
private final Label label;
private final Button button;
public Dialog(Label label, Button button) {
this.label = label;
this.button = button;
}
public void setText(String text) {
label.setText(text);
}
@Override
public Dialog cloneObj() {
return new Dialog(
label.cloneObj(),
button.cloneObj()
);
}
@Override
public String toString() {
return """
--- ダイアログ ---
%s
%s
""".formatted(label, button);
}
}
最後に DialogFactory クラスです。
コンストラクタでベースとなる Dialog を作成します。
これを Prototype (原型) とします。
そして、create メソッドでは、Prototype を元に Dialog を作成します。
public class DialogFactory {
private final Dialog prototype;
public DialogFactory() {
// ベースとなるダイアログを作成します。
final var label = new Label();
final var button = new Button();
button.setText("OK");
prototype = new Dialog(label, button);
}
public Dialog create(String text) {
final var dialog = prototype.cloneObj();
dialog.setText(text);
return dialog;
}
}
それでは、これらのクラスを使ってみましょう。
final var factory = new DialogFactory();
final var dialog1 = factory.create("処理を実行します。");
System.out.print(dialog1);
// 結果
// ↓
//--- ダイアログ ---
//ラベル : 処理を実行します。
//ボタン : OK
final var dialog2 = factory.create("エラーが発生しました。");
System.out.print(dialog2);
// 結果
// ↓
//--- ダイアログ ---
//ラベル : エラーが発生しました。
//ボタン : OK
結果も問題なしですね。
無事に、DialogFactory クラスを使って Dialog が生成できました。
不変オブジェクトの複製について
不変オブジェクトとは、簡単にいうと…
- 属性がまったく変わらないオブジェクト
のことをいいます。
(詳細については、上記の引用先のページをご参照ください)
例えば、次のようなクラスです。
public final class Sample {
private final int num;
public Sample(int num) {
this.num = num;
}
public int getNum() {
return num;
}
}
フィールドの num は変更できません。
よって Sample オブジェクトは不変です。
さて、不変オブジェクトを 複製(Clone) することを考えてみましょう。
初めに、おすすめしない例です。
public final class Sample {
private final int num;
public Sample(int num) {
this.num = num;
}
public int getNum() {
return num;
}
public Sample cloneObj() {
// ★新しいオブジェクトを生成するのは無駄です。
return new Sample(num);
}
}
cloneObj メソッドでは、Sample クラスを新しく new しています。
これでも問題なく動作はするでしょう。
しかし、パフォーマンス的には無駄な処理です。
よって、次のようにするのがおすすめです。
public final class Sample {
private final int num;
public Sample(int num) {
this.num = num;
}
public int getNum() {
return num;
}
public Sample cloneObj() {
return this;
}
}
cloneObj メソッドでは this を返します。
これでも問題はありません。
なぜなら Sample オブジェクトは不変です。
属性が変わることはないのです。
final var sample = new Sample(123);
System.out.println(sample.getNum()); // 123
final var cloned = sample.cloneObj();
System.out.println(cloned.getNum()); // 123
// インスタンスは同じ
System.out.println(sample == cloned); // true
例えば num = 123 を複製しても num = 123 です。
そして num は変更できません。
それであれば、同じインスタンスを共有しても問題ありません。
Java による clone メソッドの実装
Java の標準API である Object クラスには clone メソッドがあります。
もし Java でオブジェクトを複製した場合は、clone メソッドを使うと便利な場合があります。
レコードクラス を使ったコード例です。
record Sample(int num, String str) implements Cloneable {
@Override
public Sample clone() throws CloneNotSupportedException {
return (Sample) super.clone();
}
}
try {
final var sample = new Sample(100, "abc");
final var cloned = sample.clone();
System.out.println("Cloned! : " + cloned);
} catch (CloneNotSupportedException e) {
System.out.println("CloneNotSupportedException!");
}
// 結果
// ↓
//Cloned! : Sample[num=100, str=abc]
詳細については、以下の記事もご参照ください。
まとめ
Prototype パターンとは、
- 自身の属性を引き継いだ新しいオブジェクトを生成する
という設計です。
クラスの詳細を知らなくてもオブジェクトを複製したい、というときに使えます。
有効に活用していきたいですね。
関連記事
- 標準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 パターン
- Proxy パターン
- Singleton パターン
- State パターン
- Strategy パターン
- Template Method パターン
- Visitor パターン