Prototype
一言でいうと
Prototype (プロトタイプ、 原型) は、 既存オブジェクトのコピーをそのクラスに依存することなく可能とする、 生成に関するデザインパターンの一つです。
![Prototype デザインパターン](/images/patterns/content/prototype/prototype.png?id=e912b1ada20bbf7b2ffc09e93b9fab20)
問題
ここにオブジェクトがあって、 この正確なコピーを作成したいとします。 どうすればいいでしょう? まず、 同じクラスの新規オブジェクトを作成する必要があります。 次に、 元のオブジェクトのすべてのフィールドの値を一つずつ新しいオブジェクトにコピーしていきます。
よさそうですね! でも落とし穴があります。 どんなオブジェクトでもこの方法でコピーできるわけではないのです。 オブジェクトのフィールドの一部は private
(非公開) で、 オブジェクトの外からは見えないようになっているかもしれません。
![「外部で」コビーしてうまく行かない場合](/images/patterns/content/prototype/prototype-comic-1-ja.png?id=794900eaaa259e800e1b4f143af2e2c0)
外部からオブジェクトをコピーすることは常に可能とは限りません。
この直接的なやり方には、 もう一つ問題があります。 複製を作成するためには、 オブジェクトのクラスについてよく知っている必要があるので、 コードはそのクラスに依存するようになります。 余分な依存関係なんてへっちゃらという方には、 もう一つ別の落とし穴があります。 オブジェクトが従うインターフェースはわかっているが、 具象クラスは知らない、 という場合があります。 たとえば、 あるメソッドは、 パラメーターとして、 あるインターフェースに従うどんなオブジェクトでも受け取るといった場合です。
解決策
このパターンでは、 クローン (複製) される実際のオブジェクトにクローン作成の作業が任されます。 クローンをサポートするすべてのオブジェクトに対する共通インターフェースを宣言します。 このインターフェースを使用すると、 コードをそのオブジェクトのクラスに密に結合せずにオブジェクトのクローン作成ができます。 通常、 この手のインターフェースは、 clone
メソッド一つだけからなっています。
clone
メソッドの実装はすべてのクラスで非常に似ています。 このメソッドは現在のクラスのオブジェクトを作成し、 古いオブジェクトのすべてのフィールド値を新しいクラスに引き継ぎます。 ほとんどのプログラミング言語では、 同じクラスに属するオブジェクトは、 他のオブジェクトの非公開フィールドにアクセスできるため、 非公開フィールドをコピーすることもできます。
クローン作成をサポートするオブジェクトは、 プロトタイプと呼ばれます。 オブジェクトに数十のフィールドと数百の可能な構成がある場合、 サブクラスを作る代わりにクローン作成が有効な代替手段となるかもしれません。
![構築済みプロトタイプ](/images/patterns/content/prototype/prototype-comic-2-ja.png?id=6687d7d5c44d3ff7811c379d7628acf0)
事前に構築されたプロトタイプはサブクラス化の代替手段。
仕組み: 様々な構成のオブジェクトをあらかじめ作成しておきます。 これら構成ずみオブジェクトの一つに似たオブジェクトが必要な場合、 新しいオブジェクトを一から構築する代わりにプロトタイプをクローンします。
現実世界でのたとえ
現実世界では、 製品の大量生産を開始する前に、 様々なテストを行うためにプロトタイプが使用されます。 しかし、 この場合、 プロトタイプは実際の生産には使われません。 受動的な役割を果たすだけです。
![細胞分裂](/images/patterns/content/prototype/prototype-comic-3-ja.png?id=a0a7129addeeb0294d43826ffa493102)
細胞分裂
工業用プロトタイプは本当に自分自身をコピーしないので、 パターンに非常に近い類似は、 細胞分裂 (生物学、 覚えていますか?) のプロセスです。 分裂後、 同一細胞のペアが形成されます。 元のセルはプロトタイプとして機能し、 コピーの作成に積極的な役割を果たします。
構造
基本的な実装
![Prototype デザインパターンの構造](/images/patterns/diagrams/prototype/structure.png?id=088102c5e9785ff45debbbce86f4df81)
![Prototype デザインパターンの構造](/images/patterns/diagrams/prototype/structure-indexed.png?id=0e1c809842f5c43aca0541a2eba1f844)
-
プロトタイプ (Prototype) インターフェースはクローン作成メソッドを宣言します。 ほとんどの場合、 ただ一つの
clone
メソッドからできています。 -
具象プロトタイプ (Concrete Prototype) のクラスは、 クローン作成メソッドを実装します。 元のオブジェクトのデータをコピーするだけではなく、 このメソッドはクローン作成時にクローン同士をリンクするとか、 再起的依存性を取り払うなどの作業も行うかもしれません。
-
クライアント (Client) は、 プロトタイプのインターフェースに従うどんなオブジェクトでも、 そのコピーを生成できます。
プロトタイプ・レジストリーの実装
![プロトタイプ・レジストリー](/images/patterns/diagrams/prototype/structure-prototype-cache.png?id=609c2af5d14ed55dcbb218a00f98e7d5)
![プロトタイプ・レジストリー](/images/patterns/diagrams/prototype/structure-prototype-cache-indexed.png?id=10a4a84a1a318f59dbc2b806fc936d04)
-
プロトタイプ・レジストリー (Prototype Registry) により、 よく使われるプロトタイプに簡単にアクセスできます。 そこには、 いつでも複製可能な状態の構築ずみオブジェクトが蓄えられています。 一番簡単な実装方法は、
名前 → プロトタイプ
のハッシュマップを使うことです。 しかし、 単に名前に頼る以上の検索方式が必要な場合は、 もっと堅牢なレジストリーを作ることも可能です。
擬似コード
この例では、 Prototype パターンを使用して、 コードをクラスに密に結合することなく、 幾何学的なオブジェクトの正確なコピーを生成します。
![Prototype パターン例の構造](/images/patterns/diagrams/prototype/example.png?id=47bc6c1058cb100b81e675b5ca6bda6c)
クラス階層に属するオブジェクトの集合をクローンする。
すべての形状クラスは、 クローン作成メソッドを持つ同一インターフェースに従っています。 サブクラスは、 サブクラス特有のフィールド値を結果のオブジェクトにコピーする前に、 親のクローン作成メソッドを呼び出すこともできます。
// プロトタイプの基底クラス。
abstract class Shape is
field X: int
field Y: int
field color: string
// 通常のコンストラクター。
constructor Shape() is
// ……
// プロトタイプのコンストラクター。新鮮なオブジェクトは、既存のオブ
// ジェクトの値を使って初期化される。
constructor Shape(source: Shape) is
this()
this.X = source.X
this.Y = source.Y
this.color = source.color
// クローン操作は、Shape のサブクラスのいずれかを返す。
abstract method clone():Shape
// 具象プロトタイプ。クローン作成メソッドは、現クラスのコンストラクターを
// 呼び、引数として現オブジェクトを渡すことにより新規オブジェクトを一度に
// 作成。実際のコピー作業をコンストラクターの中で行い、結果の一貫性を保証。
// コンストラクターは完全なオブジェクトが完成するまで帰らないので、他の
// オブジェクトが部分的に作成されたクローンへの参照を持つことは不可能。
class Rectangle extends Shape is
field width: int
field height: int
constructor Rectangle(source: Rectangle) is
// 親クラスで定義された非公開フィールドのコピーを行うため、親のコ
// ンストラクターを呼ぶことは必須。
super(source)
this.width = source.width
this.height = source.height
method clone():Shape is
return new Rectangle(this)
class Circle extends Shape is
field radius: int
constructor Circle(source: Circle) is
super(source)
this.radius = source.radius
method clone():Shape is
return new Circle(this)
// クライアント・コードのどこか。
class Application is
field shapes: array of Shape
constructor Application() is
Circle circle = new Circle()
circle.X = 10
circle.Y = 10
circle.radius = 20
shapes.add(circle)
Circle anotherCircle = circle.clone()
shapes.add(anotherCircle)
// anotherCircle 変数は、circle オブジェクトの完全なコピーを含
// む。
Rectangle rectangle = new Rectangle()
rectangle.width = 10
rectangle.height = 20
shapes.add(rectangle)
method businessLogic() is
// Prototype はすごい!ここでは、オブジェクトのコピーの作成をそ
// の型について一切知らずに行う。
Array shapesCopy = new Array of Shapes.
// 我々は、shapes 配列内の要素が厳密に何であるかを知らない。わ
// かっているのは、どれも形状だということだけ。しかし、多相性のお
// かげで、形状の一つに対して clone メソッドを呼ぶと、プログラム
// がその本当のクラスが何かをチェックし、そのクラスで定義された適
// 切な clone メソッドを実行する。一連の単純な Shape オブジェク
// トではなく、適切なクローンを取得可能なのはこのため。
foreach (s in shapes) do
shapesCopy.add(s.clone())
// shapesCopy 配列には、shape 配列の子要素が正確にコピーされる。
適応性
自分のコードがコピー対象オブジェクトの具象クラスに依存したくない場合に、 Prototype パターンを使用します。
この状況は、 自分のコードが何らかのインターフェースを介して渡された外部開発コードからのオブジェクトと動作する場合によく発生します。 これらのオブジェクトの具象クラスは不明であり、 仮に望んでもそれに依存することはできません。
Prototype パターンを適応すると、 クライアントのコードは、 クローンをサポートするすべてのオブジェクトを扱う一般的なインターフェースを使います。 このインターフェースにより、 クライアントのコードは、 クローン対象オブジェクトの具象クラスから独立したものとなります。
オブジェクトの初期化方法のみが異なるサブクラスの数を減らしたい場合に、 このパターンを使用します。
使用する前に手の込んだ設定を必要とする複雑なクラスがあるとします。 このクラスを設定するには、 いくつかの共通する方法がありますが、 それはアプリのあちこちに散らばっています。 重複を減らすために、 複数のサブクラスを作成し、 共通設定コードをサブクラスのコンストラクターに入れます。 重複問題を解決しましたが、 今度はダミーのサブクラスがたくさんできました。
Prototype パターンでは、 いろいろな方法で事前構築したオブジェクトを使うことができます。 何かの構成に一致するサブクラスをインスタンス化する代わりに、 クライアントは適切なプロトタイプを探してクローンするということができます。
実装方法
-
プロトタイプのインターフェースを作成し、 そこで
clone
メソッドを宣言します。 または、 既存のクラス階層がある場合は、 階層下のすべてのクラスにメソッドを追加するだけでもかまいません。 -
プロトタイプ・クラスは、 そのクラスのオブジェクトを引数として受け取る代替コンストラクターを定義しなければなりません。 コンストラクターは、 渡されたオブジェクトから新しく作成されたインスタンスにクラスで定義されたすべてのフィールドの値をコピーする必要があります。 サブクラスを変更する場合は、 スーパークラスが非公開フィールドのクローン作成処理できるように親コンストラクターを呼び出す必要があります。
お使いのプログラミング言語がメソッドの多重定義をサポートしていない場合は、 オブジェクトのデータを複製するための特別なメソッドを定義することになります。 コンストラクターは、
new
演算子を呼び出した直後に生成されたオブジェクトを使えるため、 これを行うためのより便利な場所です。 -
クローン作成メソッドは通常、 プロトタイプのクラスの
new
演算子を呼び出すだけの一行だけです。 どのクラスもクローン作成メソッドを、new
に続けてそのクラス名が来るように上書きしなければなりません。 そうしないと、 親クラスのオブジェクトがクローンされてしまいます。 -
必要ならば、 頻繁に使用されるプロトタイプのカタログを備蓄しておく、 集中型プロトタイプ・レジストリーを作成します。
レジストリーは、 新しいファクトリー・クラスとして実装することもできますし、 またはプロトタイプを取得する静的メソッドをプロトタイプの基底クラスに追加することもできます。 このメソッドは、 クライアント・コードからメソッドに渡される検索条件に基づいてプロトタイプを検索します。 検索条件は単純な文字列によるタグかもしれませんし、 または複雑な検索パラメータの組み合わせかもしれません。 適切なプロトタイプが見つかった後、 レジストリーはそれをクローンし、 クライアントに返します。
最後に、 サブクラスのコンストラクターへの直接呼び出しを、 プロトタイプ・レジストリーのファクトリー・メソッドへの呼び出しに置き換えます。
長所と短所
- 具象クラスと密に結合せずにオブジェクトのクローンが可能。
- 構築済みのプロトタイプのクローン作成を使うことにより、 初期化コードの重複を削減。
- 複雑なオブジェクトの生成がより便利。
- 複雑なオブジェクトに対する構成の事前設定を扱う上で継承に代わる方法を提供。
- 循環参照のある複雑なオブジェクトのクローン作成は一筋縄ではいかない場合あり。
他のパターンとの関係
-
多くの設計は、 まず比較的単純でサブクラスによりカスタマイズ可能な、 Factory Method から始まり、 次第に、 もっと柔軟だが複雑な Abstract Factory や Prototype や Builder へと発展していきます。
-
Abstract Factory クラスは、 多くの場合 Factory Methods の集まりですが、 Prototype を使ってメソッドを書くこともできます。
-
Composite と Decorator を多用する設計に対しては、 Prototype の使用が有益かもしれません。 このパターンを適用すると、 複雑な構造を初めから再構築するのではなく、 それをクローンします。
-
Prototype は継承に基づいていないので、 継承の欠点はありません。 一方、 Prototype は、 クローンされたオブジェクトの複雑な初期化が必要となります。 Factory Method は継承に基づいていますが、 初期化のステップは必要ありません。
-
場合によっては、 Prototype を Memento の代わりに使用した方が簡単な場合があります。 状態の履歴を保存したいオブジェクトが比較的単純で、 他の外部リソースへのリンクを持たないか簡単に再現できる場合に、 この方法が使えます。
-
Abstract Factories、 Builders、 Prototypes はどれも Singletons で実装可能です。