Prototype
概要
ディープコピーで複製したオブジェクトたち
newするのにはものすっごい費用がかかるとまず認識する.
例えば、「計算結果」というプロパティオブジェクトを考える
これは引数xを指定するとxについて足したりかけたりなんじゃかんじゃと難しい計算をして
その挙句5とか3とかのシンプルな整数をその計算の解として持つ。
そのオブジェクトを引数2としてnewするとクソ長い計算の果てに計算結果= 4を得るとする。
このオブジェクトがもう一つ欲しくなった時、再びnewするとクソ長い計算がまた始まる。
それより、演算結果がすでにわかったのだから、計算結果に4を持つ同じインスタンスをnewせず複製すれば一瞬で終わる話。
生成したらやりっぱなし放置しておけるインスタンスに使えるパターン。
必要条件は
- ミュータブルであること
- 互いに異なるオブジェクトであること
イミュータブルならそれはグローバル変数だし ミュータブルでも互いに同じならそれは同じ動きしかできない。棒の両端にマネキンつけるアレ。
マンガでわかる Prototype
マンガでわかる Prototype #デザインパターン - Qiita
でざぱたんで覚える Prototype
ちびキャラは「プロトタイプたん」。クローンを量産される「始まりの生体」という重めの設定で、分身がいくら切り刻まれても本体は無傷=コピーは元オブジェクトから独立している、というPrototypeの核を表す。既定値を仕込んだ「母」オブジェクトを一体用意してクローンを取り回すのが基本形だが、想定外の値まで一緒に複製されるバグの温床にもなるので「ご利用は計画的に」。
出典: いしだけ『でざぱたん: ちびキャラで覚えるデザインパターン』(P.072〜)
メリット(用途)
インスタンス生成における以下のような事例
- オブジェクトの種類が多すぎてクラスにまとめられないとき
- 図形作成オブジェクトとして、その気になれば図形の種類に応じていくらでもクラスファイルが生成しうる場合
- クラスからのインスタンス生成が難しいとき
- めちゃくちゃ複雑な過程から生成されたインスタンスが複数必要なら、一つ作ったものを保存しておいてコピーする
- フレームワークと生成するインスタンスを分離したいとき
コピー機は元の書類をどうやって作ったか知らなくても、コピーをかければ同じ書類を何枚でも作れる
やり方
- コピーしたいクラスにディープコピーするメソッドを用意
- Javaなら
java.lang.Clonableを実装してclone() - ただしcloneはシャローコピーであり、例えば配列プロパティは参照だけがコピーされるので、クローン元の配列プロパティを書き換えるとクローン先も書き換わるため、適宜ディープコピーをオーバーライド
クラス図
このサイトの実装(装飾文字列/下線ペン)での対応関係:
classDiagram
class Product {
<<interface>>
+use(s)
+createClone() Product
}
class Manager {
-showcase
+register(name, proto)
+create(protoname) Product
}
class MessageBox {
-decochar
+use(s)
+createClone() Product
}
class UnderlinePen {
-ulchar
+use(s)
+createClone() Product
}
Product <|.. MessageBox
Product <|.. UnderlinePen
Manager o-- Product
関連
Java
import framework.*;
public class MessageBox implements Product {
private char decochar;
public MessageBox(char decochar) {
this.decochar = decochar;
}
public void use(String s) {
int length = s.getBytes().length;
for (int i = 0; i < length + 4; i++) {
System.out.print(decochar);
}
System.out.println("");
System.out.println(decochar + " " + s + " " + decochar);
for (int i = 0; i < length + 4; i++) {
System.out.print(decochar);
}
System.out.println("");
}
public Product createClone() {
Product p = null;
try {
p = (Product)clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return p;
}
}
package framework;
import java.lang.Cloneable;
public interface Product extends Cloneable {
public abstract void use(String s);
public abstract Product createClone();
}
package framework;
import java.util.*;
public class Manager {
private HashMap showcase = new HashMap();
public void register(String name, Product proto) {
showcase.put(name, proto);
}
public Product create(String protoname) {
Product p = (Product)showcase.get(protoname);
return p.createClone();
}
}
import framework.*;
public class UnderlinePen implements Product {
private char ulchar;
public UnderlinePen(char ulchar) {
this.ulchar = ulchar;
}
public void use(String s) {
int length = s.getBytes().length;
System.out.println("\"" + s + "\"");
System.out.print(" ");
for (int i = 0; i < length; i++) {
System.out.print(ulchar);
}
System.out.println("");
}
public Product createClone() {
Product p = null;
try {
p = (Product)clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return p;
}
}
import framework.*;
public class Main {
public static void main(String[] args) {
// 準備
Manager manager = new Manager();
UnderlinePen upen = new UnderlinePen('~');
MessageBox mbox = new MessageBox('*');
MessageBox sbox = new MessageBox('/');
manager.register("strong message", upen);
manager.register("warning box", mbox);
manager.register("slash box", sbox);
// 生成
Product p1 = manager.create("strong message");
p1.use("Hello, world.");
Product p2 = manager.create("warning box");
p2.use("Hello, world.");
Product p3 = manager.create("slash box");
p3.use("Hello, world.");
}
}
Go
Goに Cloneable / Object.clone() は無い。CreateClone を interface で自分で要求し、各型が clone := *m; return &clone(構造体コピー)で複製する。浅い/深いコピーを型ごとに自分で選ぶのがGo流。Manager は具体型名を知らずに複製で製品を増やす。
実行: go run ./GoF/patterns/Prototype/go
package main
// ---- framework 層(Java版の framework パッケージに相当)----
// Product はプロトタイプ(複製元)になれる製品の抽象。
// Java版は Cloneable を継承するが、Go に Cloneable は無いので
// CreateClone を interface のメソッドとして自分で要求する。
type Product interface {
Use(s string)
CreateClone() Product
}
// Manager はプロトタイプを名前で登録し、複製を作る役。
// 具体的な型名を一切知らずに(Product interface だけで)製品を増やせるのが肝。
// 「クラスからインスタンスを作る」のでなく「インスタンスからインスタンスを作る」。
type Manager struct {
showcase map[string]Product
}
func NewManager() *Manager {
return &Manager{showcase: make(map[string]Product)}
}
func (m *Manager) Register(name string, proto Product) {
m.showcase[name] = proto
}
func (m *Manager) Create(protoName string) Product {
return m.showcase[protoName].CreateClone()
}
package main
import (
"fmt"
"strings"
)
// MessageBox は装飾文字で文字列を囲む Product。
type MessageBox struct {
decoChar rune
}
func NewMessageBox(decoChar rune) *MessageBox {
return &MessageBox{decoChar: decoChar}
}
func (m *MessageBox) Use(s string) {
length := len(s) // Java版 s.getBytes().length と同じくバイト長
border := strings.Repeat(string(m.decoChar), length+4)
fmt.Println(border)
fmt.Printf("%c %s %c\n", m.decoChar, s, m.decoChar)
fmt.Println(border)
}
// CreateClone は自分自身の浅いコピーを返す。
// Java版は Object.clone() を使うが、Go に汎用 clone は無いので型ごとに書く。
// フィールドが値型(rune)だけなので、構造体のコピーで十分な複製になる。
// (もしポインタやスライスを持つなら、深いコピーが要るか自分で判断する)
func (m *MessageBox) CreateClone() Product {
clone := *m
return &clone
}
package main
import (
"fmt"
"strings"
)
// UnderlinePen は文字列に下線を引く Product。
type UnderlinePen struct {
ulChar rune
}
func NewUnderlinePen(ulChar rune) *UnderlinePen {
return &UnderlinePen{ulChar: ulChar}
}
func (u *UnderlinePen) Use(s string) {
length := len(s)
fmt.Printf("\"%s\"\n", s)
fmt.Println(" " + strings.Repeat(string(u.ulChar), length))
}
func (u *UnderlinePen) CreateClone() Product {
clone := *u
return &clone
}
package main
// 実行: go run ./GoF/patterns/Prototype/go
//
// 見どころ: manager は MessageBox/UnderlinePen という型名を知らない。
// 登録済みインスタンスを複製するだけで、新しい種類の“製品”を名前で増やせる。
func main() {
// 準備: プロトタイプを作って名前で登録
manager := NewManager()
upen := NewUnderlinePen('~')
mbox := NewMessageBox('*')
sbox := NewMessageBox('/')
manager.Register("strong message", upen)
manager.Register("warning box", mbox)
manager.Register("slash box", sbox)
// 生成: 名前から複製を作って使う
p1 := manager.Create("strong message")
p1.Use("Hello, world.")
p2 := manager.Create("warning box")
p2.Use("Hello, world.")
p3 := manager.Create("slash box")
p3.Use("Hello, world.")
}
PHP
<?php
/**
* クローンをいじろうが元のオブジェクトに影響を与えない
*
* PHPにはもともとcloneの機能があるのでそれを用いる
* cloneの機能がない言語においては
* 明示的に深いコピーをしたインスタンスを返す
* つまり、ディープコピーしないと予期せぬバグの原因になりますよということ
*
*
*/
class Prototype
{
private $values = array();
public function __set($key, $val)
{
$this->values[$key] = $val;
}
public function __get($key)
{
return $this->values[$key];
}
/**
* phpのコピーはシャローコピーなので
* 深いコピーを作成するためにマジックメソッドcloneを作る
*
*/
public function __clone()
{
foreach ($this->values as $key => $val) {
if (is_object($val)) {
$this->key = clone $val;
} else {
$this->key = $val;
}
}
}
}
class Child
{
private $values = array();
public function __set($key, $val)
{
$this->values[$key] = $val;
}
public function __get($key)
{
return $this->values[$key];
}
}
function say($i)
{
print "$i <br>";
}
// オリジナル作成
$originalObj = new Prototype();
$originalObj->hoge = "fuga";
$cloneObj = clone $originalObj;
say($originalObj->hoge === $cloneObj->hoge ? "OK" : "NG"); //OK
// クローンにピヨを代入
$cloneObj->hoge = "piyo";
//クローンにはhogeがあって、
// オリジナルにはpiyoがある
say($originalObj->hoge == $cloneObj->hoge ? "OK" : "NG"); //NG;
var_dump($originalObj->hoge);
var_dump($cloneObj->hoge);
$originalObj->child = new Child();
$originalObj->child->xyzzy = "vim";
$cloneObj = clone $originalObj;
say($originalObj->child->xyzzy == $cloneObj->child->xyzzy ? "OK" : "NG"); //OK
// cloneのchildのxyzzyにemacsを代入
$cloneObj->child->xyzzy = "emacs";
// オリジナルとクローンが正しくディープコピーされている
// マジックメソッドcloneを上書きしておかないと
// オリジナルもemacsになるので気をつける
// 以下は、Childには__cloneしてないので、
// childのプロパティがシャローコピーされてしまった例
say($originalObj->child->xyzzy == "vim" ? "OK" : "NG"); // NG
say($cloneObj->child->xyzzy == "emacs" ? "OK" : "NG"); //OK
var_dump($originalObj->child);
var_dump($cloneObj->child);
TypeScript
JS/TSにもObject.clone()相当は無い。Go版と同じく、各Productが自分でnewし直して複製する(createClone())。
実行: npx tsx GoF/patterns/Prototype/typescript/main.ts
// ---- framework 層(Java版の framework パッケージに相当)----
//
// TypeScript の interface は Java の interface とほぼ同じ感覚で書ける(Strategy版と同じ理由)。
// ただし Java の Product は Cloneable を継承して「複製できる」ことを型で保証しているのに対し、
// TypeScript(JavaScript) には Cloneable のようなマーカーインタフェースも Object.clone() のような
// 汎用複製も無い。そこで Go版と同じ発想で、createClone() を interface のメソッドとして
// 自分で要求する形にする。
export interface Product {
use(s: string): void;
createClone(): Product;
}
// Manager はプロトタイプを名前で登録し、複製を作る役。
// 具体的な型(MessageBox/UnderlinePen)を一切知らずに Product interface だけで
// 製品を増やせるのが Prototype パターンの肝。
// 「クラスからインスタンスを作る」のではなく「インスタンスからインスタンスを作る」。
export class Manager {
private readonly showcase = new Map<string, Product>();
register(name: string, proto: Product): void {
this.showcase.set(name, proto);
}
create(protoName: string): Product {
const proto = this.showcase.get(protoName);
if (!proto) {
throw new Error(`prototype not found: ${protoName}`);
}
return proto.createClone();
}
}
// 実行方法は main.ts を参照。
import type { Product } from "./framework";
// MessageBox は装飾文字で文字列を囲む Product。
export class MessageBox implements Product {
constructor(private readonly decoChar: string) {}
use(s: string): void {
// Java版は s.getBytes().length、Go版は len(s) でどちらもバイト長。
// TypeScript の string.length は UTF-16 コード単位数でバイト長と一致しないため、
// Node.js の Buffer で明示的にバイト長を測って挙動を揃える。
const length = Buffer.byteLength(s);
const border = this.decoChar.repeat(length + 4);
console.log(border);
console.log(`${this.decoChar} ${s} ${this.decoChar}`);
console.log(border);
}
createClone(): Product {
// JS/TS には Java の Object.clone() のような汎用の複製機構が無い。
// Go版が「構造体をコピーして返す」のと同じ発想で、フィールドを渡して
// 新しいインスタンスを組み立て直す(フィールドが文字列だけなので浅いコピーで十分)。
return new MessageBox(this.decoChar);
}
}
// 実行方法は main.ts を参照。
import type { Product } from "./framework";
// UnderlinePen は文字列に下線を引く Product。
export class UnderlinePen implements Product {
constructor(private readonly ulChar: string) {}
use(s: string): void {
const length = Buffer.byteLength(s);
console.log(`"${s}"`);
console.log(" " + this.ulChar.repeat(length));
}
createClone(): Product {
return new UnderlinePen(this.ulChar);
}
}
// Prototype パターン: MessageBox / UnderlinePen (Java版と同じ題材)
//
// 実行:
// npx tsx main.ts
//
// 見どころ: manager は MessageBox/UnderlinePen という型名を知らない。
// 登録済みインスタンスを複製するだけで、新しい種類の"製品"を名前で増やせる。
import { Manager } from "./framework";
import { MessageBox } from "./messagebox";
import { UnderlinePen } from "./underlinepen";
function main(): void {
// 準備: プロトタイプを作って名前で登録
const manager = new Manager();
const upen = new UnderlinePen("~");
const mbox = new MessageBox("*");
const sbox = new MessageBox("/");
manager.register("strong message", upen);
manager.register("warning box", mbox);
manager.register("slash box", sbox);
// 生成: 名前から複製を作って使う
const p1 = manager.create("strong message");
p1.use("Hello, world.");
const p2 = manager.create("warning box");
p2.use("Hello, world.");
const p3 = manager.create("slash box");
p3.use("Hello, world.");
}
main();
Python
Pythonには標準のcopyモジュールがあるため、create_clone()の中でcopy.copy(浅いコピー)を使う。Manager側は複製方法を知らず、Productに委ねる点はJava/Go版と同じ。
実行: python3 GoF/patterns/Prototype/python/main.py
"""Prototype パターンの framework 層(Java版の framework パッケージに相当)。
Java版の Product は Cloneable を継承し、複製できることを型で保証している。
Python には Cloneable のようなマーカーは無いが、interface に相当する言語機能として
abc モジュールの抽象基底クラス(ABC)が使える(Strategy版と同じ考え方)。
標準に copy.deepcopy はあるものの、それだと「どのプロパティを複製すべきか」を
Product 任せにできず Manager が具象型の内部構造を意識してしまう。
そこで Java/Go版と同じく createClone() を抽象メソッドとして要求し、
複製方法(浅いコピーで足りるか、深いコピーが要るか)は各 Product 自身に決めさせる。
"""
from __future__ import annotations
from abc import ABC, abstractmethod
class Product(ABC):
@abstractmethod
def use(self, s: str) -> None: ...
@abstractmethod
def create_clone(self) -> "Product": ...
class Manager:
"""プロトタイプを名前で登録し、複製を作る役。
具体的な型(MessageBox/UnderlinePen)を一切知らずに Product のインタフェースだけで
製品を増やせるのが Prototype パターンの肝。
「クラスからインスタンスを作る」のではなく「インスタンスからインスタンスを作る」。
"""
def __init__(self) -> None:
self._showcase: dict[str, Product] = {}
def register(self, name: str, proto: Product) -> None:
self._showcase[name] = proto
def create(self, proto_name: str) -> Product:
proto = self._showcase[proto_name]
return proto.create_clone()
"""実行方法は main.py を参照。"""
from __future__ import annotations
import copy
from framework import Product
class MessageBox(Product):
"""装飾文字で文字列を囲む Product。"""
def __init__(self, deco_char: str) -> None:
self._deco_char = deco_char
def use(self, s: str) -> None:
# Java版は s.getBytes().length(バイト長)。ここでもUTF-8バイト長に揃える。
length = len(s.encode("utf-8"))
border = self._deco_char * (length + 4)
print(border)
print(f"{self._deco_char} {s} {self._deco_char}")
print(border)
def create_clone(self) -> Product:
# フィールドが文字列(イミュータブル)だけなので copy.copy で浅いコピーで十分。
# フィールドに可変オブジェクト(list等)を持つ Product を作る場合は
# copy.deepcopy に切り替える必要がある、という判断をここでしている。
return copy.copy(self)
"""実行方法は main.py を参照。"""
from __future__ import annotations
import copy
from framework import Product
class UnderlinePen(Product):
"""文字列に下線を引く Product。"""
def __init__(self, ul_char: str) -> None:
self._ul_char = ul_char
def use(self, s: str) -> None:
length = len(s.encode("utf-8"))
print(f'"{s}"')
print(" " + self._ul_char * length)
def create_clone(self) -> Product:
return copy.copy(self)
"""Prototype パターン: MessageBox / UnderlinePen (Java版と同じ題材)
実行:
python3 main.py
見どころ: manager は MessageBox/UnderlinePen という型名を知らない。
登録済みインスタンスを複製するだけで、新しい種類の"製品"を名前で増やせる。
Python標準には copy.deepcopy があるので「clone()を自作せずそれだけで良いのでは」
とも思えるが、それだと Manager 側が「この Product はどう複製すべきか」を知らないまま
何でも deepcopy してしまい、複製方法を Product 自身が決める Prototype パターンの意図が
薄れる。ここでは Java/Go版に倣い createClone() を Product に持たせ、内部で
copy モジュール(copy.copy)を呼ぶ形にした。
"""
from __future__ import annotations
from framework import Manager
from messagebox import MessageBox
from underlinepen import UnderlinePen
def main() -> None:
# 準備: プロトタイプを作って名前で登録
manager = Manager()
upen = UnderlinePen("~")
mbox = MessageBox("*")
sbox = MessageBox("/")
manager.register("strong message", upen)
manager.register("warning box", mbox)
manager.register("slash box", sbox)
# 生成: 名前から複製を作って使う
p1 = manager.create("strong message")
p1.use("Hello, world.")
p2 = manager.create("warning box")
p2.use("Hello, world.")
p3 = manager.create("slash box")
p3.use("Hello, world.")
if __name__ == "__main__":
main()