広告

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

Prototype パターンとは、GoF によって定義されたデザインパターンの1つです。
自身の属性を引き継いだ新しいオブジェクトを生成します。

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


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

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

概要

Prototype パターン(英: Prototype pattern、プロトタイプ・パターン)とは、ソフトウェア開発で用いられる、生成に関するデザインパターンの1つである。生成されるオブジェクトの種別がプロトタイプ(典型)的なインスタンスであるときに使用され、このプロトタイプを複製して新しいオブジェクトを生成する。

Prototype パターンとは、

  • 自身の属性を引き継いだ新しいオブジェクトを生成する

という設計です。

Prototype は、日本語的に発音すると「プロトタイプ」となります。
意味は「原型」または「試作品」ですね。

この記事では、属性を引き継いだオブジェクトを生成することを「複製」と表記します。


【Prototype パターンのイメージ】

イメージ図1

ある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);
    }
}

さて、このオブジェクトを複製したいとしましょう。
外部から属性にアクセスできないので、どうしたものか…

やりたいことの本質は、

  • クラスの詳細(属性) を知らなくてもオブジェクトを複製したい

です。


イメージ図2

その答えの1つが Prototype パターンです。
Prototype パターンでは、複製したいオブジェクトに

  • clone (複製)

メソッドを持たせます。

外部に公開していない属性であっても、自分自身 であればアクセスできます。
そのため、属性が同じオブジェクトでも作れるわけですね。


イメージ図3

もう1つのケースを考えてみましょう。
クライアントにはインタフェースだけが 公開 されていて、その実装クラスは 非公開 です。

さて、クライアントは、このオブジェクトを複製したいとしましょう。

今度は、外部から属性にアクセスできないだけではなく、

  • 実装クラスを new できない

という問題もあります。
(実装クラスA、実装クラスBが非公開のため)

やりたいことの本質は先ほどと同じで、

  • クラスの詳細(実装クラス) を知らなくてもオブジェクトを複製したい

です。


イメージ図4

このケースも Prototype パターンが使えます。
公開されているインタフェースに

  • clone (複製)

メソッドを宣言すれば OK ですね。
実際の処理では、実装クラスA、実装クラスBで、それぞれオブジェクトが複製されます。

関連記事:


【補足】

Java をある程度使ったことのあるかたは、

  • Object クラスの clone メソッド

をご存じかもしれません。
その名のとおり、オブジェクトを複製するメソッドですね。

Object クラスの clone メソッドについては 後ほど 軽く解説します。


クラス図とシーケンス図

クラス図1

シーケンス図1

上の図は、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 を考えてみます。

UI図1

とはいえ、GUI をコード例にするのは複雑になるので、ここでは単純なテキスト出力のみで考えます。
(あくまで上記のダイアログはイメージということで…)

ダイアログは、UI の部品として

  • ラベル : 「処理を実行します。」
  • ボタン : 「OK」

を持ちます。
(ラベルとボタンの文字列は変更可能とします)

通常のアプリであれば、いろいろなダイアログが必要となるでしょう。
そのたびに、

  1. ラベルを生成
  2. ボタンを生成
  3. 上記の部品を使ってダイアログを生成

とするのは手間がかかります。

クラス図2

そこで、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 が生成できました。


不変オブジェクトの複製について

不変オブジェクトとは、オブジェクト作成後にその状態が決して変わらないオブジェクトです。
Javaの標準APIでは不変オブジェクトが積極的に採用されています。

不変オブジェクトとは、簡単にいうと…

  • 属性がまったく変わらないオブジェクト

のことをいいます。
(詳細については、上記の引用先のページをご参照ください)

例えば、次のようなクラスです。

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 メソッドの実装

protected Object clone() throws CloneNotSupportedException
このオブジェクトのコピーを作成して、返します。

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 パターンとは、

  • 自身の属性を引き継いだ新しいオブジェクトを生成する

という設計です。
クラスの詳細を知らなくてもオブジェクトを複製したい、というときに使えます。

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


関連記事

ページの先頭へ