本記事ではGoFのデザインパターンのオブジェクトの生成に関するパターンの一つである「Prototype」パターンを解説します。このパターンを一言で説明するならば、「クラスを元にオブジェクトを生成するのではなく、オブジェクトから別のオブジェクトを生成(コピー)するパターン」と言えるでしょう。文章では想像がつきにくいと思いますので、例を踏まえながら解説していきます。
「Prototype」パターンとは
先述したように、「Prototype」パターンとはクラスを元にオブジェクトを生成するのではなく、オブジェクトから別のオブジェクトを生成(コピー)するパターンです。新しくオブジェクトを立てるより、一番初めに作ったオブジェクトと同じオブジェクトが大量に欲しい場合もあると思います。そのようなときに威力を発揮するパターンです。
オブジェクトの初期化に時間がかかるような場合で有効で、何度もnewしていると処理時間がどんどん増加するため、最初の原本を保存しておき、それをコピーすることでオブジェクトの生成時間を短縮します。
また単純なクラスを大量に生成する場合にも有効です。例えば、PowerPointやCADのツールで四角形を描画するときに、何度もnewしていては効率が悪いです。一度描画した四角形のオブジェクトを既存の四角形として保存しておけば、次に四角形を描画するときに保存したオブジェクトをコピーすれば同じものが出来上がります。
「Prototype」の書き方
ではサンプルケースを踏まえて解説していきます。あなたは個人でやっている駆け出しの漫画家です。今日はコミックマーケット(コミケ)に自分の書いた本を売りにいきます。ネットで漫画を公開しているため、そこそこの売り上げは見込めそうです。そこで100部作って売ることにしました。この状況をコードベースで考えてみます。
✖︎ Prototypeパターンを使用しない例 ✖︎
まずは、Prototypeパターンを利用しない方法です。一冊の漫画をクラスとして定義し、そのクラスから生成されるオブジェクトを大量に作っていくことを考えます。下記はMangaクラスです。Mangaクラスには、漫画のタイトルや漫画のページ等のフィールドを持っています。何のフィールドを持っているかはあまり重要ではないのでここでは省略します。また漫画の初期化を行うためのコンストラクタを持っています。今回はタイトルだけ設定できるようにします。
public class Manga {
public String title;
public String[] page;
:
:
// コンストラクタ
public Manga(String title) {
this.title = title;
}
}
あなたは漫画家なのでMangaArtistクラスとし、大量の漫画を作るメソッドと一冊の漫画を作るメソッドを持っています。createMangaでは漫画の書き方の手順であるメソッドを複数使っており、一つ一つがかなり時間のかかる作業です。
public class MangaArtist {
public Manga[] createManyManga() {
Manga[] mangas = new Manga[100];
for (int i=0; i<100; i++) {
Manga manga = new Manga("進撃の小人");
createManga(manga);
}
return mangas;
}
public void createManga(Manga manga) {
// 漫画を1から作るには時間がかかる
drawName(manga);
drawSketch(manga);
drawPen(manga);
pasteTone(manga);
bindBook(manga);
}
private void drawName(Manga manga) {
// 漫画のネームを書く
}
private void drawSketch(Manga manga) {
// 下絵を書く
}
private void drawPen(Manga manga) {
// ペン入れをする
}
private void pasteTone(Manga manga) {
// トーンを貼る
}
private void bindBook(Manga manga) {
// 製本する
}
}
注目して欲しいのがcreateManyManga()内のfor文です。for文内で一冊一冊すべての作業を行なっています。これを現実世界の言葉で表すならば、「コピー機を持っていないため、全ての作業を手作業で行なっている」ということになります。恐ろしいですね、、、一冊作るだけでも数日かかるのに、100冊作ろうとしたら何日かかるのでしょうか、、実に非効率的です。
⭕️ Prototypeパターンを使用する例 ⭕️
現実には100冊を手書きすることは絶対にありませんから、コピー機を使ってみましょう。コピーするための仕組みを定義する必要があります。Cloneableインタフェースを作って、Mangaクラスがこれを実装します。
public interface Cloneable {
public Cloneable createClone();
}
public class Manga implements Cloneable {
// 漫画のタイトル
public String title;
// 漫画のページ
public String[] page;
// インターフェースのメソッドの実装
@Override
public Cloneable createClone() {
Manga newManga = new Manga();
newManga.setTitle(this.title);
newManga.setPage(this.page);
return newManga;
}
// 引数ありコンストラクタ
public Manga(String title) {
this.title = title;
}
// 引数なしコンストラクタ
public Manga() {}
// setter
public void setTitle(String title) {
this.title = title;
}
// setter
public void setPage(String[] page) {
this.page = page;
}
}
いろいろ書いておりますが、コンストラクタとフィールドにアクセスするためのセッターです。MangaクラスはCloneableクラスを継承しているのでcreateClone()メソッドを実装していきます。createCloneはコピーを作成するメソッドです。ここでコピーする内容を決めます。今回はタイトルとページ全てをコピーします。
ここで肝心なのはコピーをするメソッド内で呼ばれるコンストラクタは、引数なしのコンストラクタとしてください。理由はここで引数を設定してフィールドに値を入れてしまうと、コピー元と違う内容になる可能性があるためです。
次に漫画家クラスを変更します。
public class MangaArtist {
public Manga[] createManyManga() {
Manga[] mangas = new Manga[100];
// 原本を作る
Manga manga = new Manga("進撃の小人");
createManga(manga);
for (int i=0; i<100; i++) {
// 100冊コピーしてリストに詰める
mangas[i] = (Manga) manga.createClone();
}
return mangas;
}
public void createManga(Manga manga) {
// このメソッド内はかなり時間がかかる
drawName(manga);
drawSketch(manga);
drawPen(manga);
pasteTone(manga);
bindBook(manga);
}
// これより下はPrototypeパターンを使用しない例と同様のため省略
}
createManga()メソッドやその中で利用されたメソッドはPrototypeパターンを使用しない例と同様のため省略しています。
先程の例ではfor文の中にcreateManga()を呼んで一冊ずつ作っていましたが、今回はfor文の外で呼ばれています。ここで漫画の原本ができています。この原本を元にmanga.createClone()でMangaのコピーを100冊作って配列に入れています。ここで注意して欲しいのが、createClone()の戻り値はあくまでCloneableクラスです。そのためMangaクラスの型にキャストする必要があります。これで時間のかかるcreateManga()は一回呼ばれるだけで完了しています。
もう少し発展させていきましょう。コミケで売ったことをきっかけに、この漫画が大ヒットしました。さらに増刷して欲しいとのことで、PCに保存しておいた漫画の原本を引っ張り出してさらに印刷することになりました。このPCに保存する動作をコードベースで実装します。
import java.util.HashMap;
import java.util.Map;
// 漫画の原本を保管するマネージャー
public class PrototypeManager {
private final Map<String, Cloneable> prototypeMap;
public PrototypeManager() {
this.prototypeMap = new HashMap<>();
}
// 漫画をkeyで保管する
public void addPrototype(String key, Cloneable protoType) {
this.prototypeMap.put(key,protoType);
}
// keyを元に漫画のコピーを返却する
public Cloneable getClone(String key) {
return prototypeMap.get(key).createClone();
}
}
漫画の原本をプロトタイプとして保存します。PrototypeManagerクラスは、Map<String, Cloneable>のprototypeMapを持っています。ここに漫画の原本をkeyと一緒に保存します。またこのクラスには漫画をkeyで保存するaddPrototype()メソッドと、keyを元に原本を探してコピーを返却するgetCloneを持っています。
この保管機能を持ったPrototypeManagerクラスを利用して、漫画家クラスを変更します。
public class MangaArtist {
public Manga[] createManyManga() {
Manga[] mangas = new Manga[100];
PrototypeManager manager = new PrototypeManager();
// 『進撃の小人』の原本を作る
Manga manga = new Manga("進撃の小人");
createManga(manga);
// PrototypeManagerを使って原本を保存する
manager.addPrototype("進撃の小人", manga);
// keyを元に漫画を持ってくる
Cloneable cloneable = manager.getClone("進撃の小人");
for (int i=0; i<100; i++) {
// 100冊コピーしてリストに詰める
mangas[i] = (Manga) cloneable.createClone();
}
return mangas;
}
public void createManga(Manga manga) {
// 漫画を1から作るには時間がかかる
drawName(manga);
drawSketch(manga);
drawPen(manga);
pasteTone(manga);
bindBook(manga);
}
// これより下はPrototypeパターンを使用しない例と同様のため省略
}
処理の流れとしては「漫画の原本を作る」→「原本をPrototypeManagerに保存する」→「keyを元に原本のコピーを取得する」→「そのコピーからさらに100冊コピーする」このような処理になっています。漫画のコピーを行うのは漫画家だけとは限りません。編集者だったり、アシスタントだったり、多くの人がコピーすることが考えられます。その多くの人が、漫画を保存しているデータベース等にアクセスできれば、keyひとつで漫画のコピーを行うことができます。
「Prototype」パターンはこのように、保存、引き出し、コピーを行なってくれるパターンです。
まとめ
本記事で利用したサンプルケースのクラス図は下記になります。
「Prototype」パターンはクラスを元にオブジェクトを生成するのではなく、オブジェクトから別のオブジェクトを生成(コピー)するパターンです。是非利用してみてください。
本記事で利用したコードは下記で公開しております。
コメント