🤖

Prototypeデザインパターンの整理

2024/11/02に公開

概要

Prototypeデザインパターンとは、オブジェクトを完璧に複製するためのデザインパターン。
オブジェクトを複製しようとすると、以下のような課題がある

  • privateなフィールドはコピーできない
  • 複製元の型を知っていないといけない
    • 複製元の型に応じてコードの変更が必要になったりする
  • 元のオブジェクトが実装しているインターフェースが不明

解決策として、複製されるオブジェクトが自分自身を複製するようにメソッドを移譲する(複製されうるクラスに持たせる共通のインターフェースを用意する)。

実装

ベーシックなPrototypeパターン

type Person struct {
	Name string
	Age  int
}

func (p Person) Clone() Person {
	return Person{Name: p.Name, Age: p.Age}
}

func main() {
	taro := Person{Name: "太郎", Age: 20}
	// 直前に定義したオブジェクト(taro)をコピーして新しいprotoTaroを作成する
	protoTaro := taro.Clone()
	fmt.Printf("protoTaro's name is %s, age is %d\n", protoTaro.Name, protoTaro.Age)
}

PrototypeRegistryの利用

以上がPrototypeパターンだが、さらにPrototypeRegistryというものがよく使われる。
これは、Prototypeをその場で作り出すのではなく、事前にテンプレートになるような値を登録しておき、後で使いまわす。

これは、Prototypeがもっと複雑な属性を持っている場合にわかりやすい。
例えば、以下のコードにおいて、太郎とその家族のデータ(PersonalInfo)をまとめて生成したいとする。
このとき、太郎を定義してClone()して編集すれば要件は一応満たせる。

taro_with_parents.go
type PersonalInfo struct {
	FirstName string
	LastName  string
	Age       int
	Address   string
	ZipCode   string
	Landline  string
}

func (pi PersonalInfo) Clone() PersonalInfo {
	return PersonalInfo{
		FirstName: pi.FirstName,
		LastName:  pi.LastName,
		Age:       pi.Age,
		Address:   pi.Address,
		ZipCode:   pi.ZipCode,
		Landline:  pi.Landline,
	}
}

func main() {
	taroInfo := PersonalInfo{
		FirstName: "太郎",
		LastName:  "山田",
		Age:       20,
		Address:   "東京都千代田区〇〇-1",
		ZipCode:   "100-0000",
		Landline:  "03-xxxx-xxxx",
	}

	// 太郎の情報を元に零子の情報を作成
	reikoInfo := taroInfo.Clone()
	reikoInfo.FirstName = "Reiko"
	reikoInfo.Age = 40

	// 太郎の情報を元に一郎の情報を作成
	ichiroInfo := taroInfo.Clone()
	ichiroInfo.FirstName = "一郎"
	ichiroInfo.Age = 42
}

しかし、太郎に子供(一太郎)が生まれ、3世帯家族になったらどうなる?

taro_with_children.go
// taroInfoが不可視な場所で定義されていたら、ichitaroInfoをゼロから定義し直す。
ichitaroInfo := PersonalInfo{
    FirstName: "一太郎",
    LastName:  "山田",
    Age:       0,
    Address:   "東京都千代田区〇〇-1",
    ZipCode:   "100-0000",
    Landline:  "03-xxxx-xxxx",
}

これは、taroInfoを適切な可視範囲で定義すれば解決する問題ではある。
しかし、太郎の子孫がどのくらい繁栄するかわからない中で、適切な可視範囲を維持し続けるのは難しい。
また、山田一族では太郎を基準とすることを明確にしたい。そこで、PrototypeRegistryを導入する。

type PrototypeRegistry map[string]PersonalInfo

func (pr PrototypeRegistry) Clone(name string) PersonalInfo {
	pi, ok := pr[name]
	if !ok {
		fmt.Printf("person %s is not registered\n", name)
	}
    
	return PersonalInfo{
        FirstName: pi.FirstName,
        LastName: pi.LastName,
        Age: pi.Age,
        Address: pi.Address,
        ZipCode: pi.ZipCode,
        Landline: pi.Landline,
    }
}

こうすることで、PrototypeRegistryに太郎を登録しておけば、一族の基準が太郎であることを覚えておけば、いつでもそれに基づくPersonalInfoを生成できる。

pr := make(PrototypeRegistry)
pr["taro"] = taroInfo

ztaro := pr.Clone("taro")
ztaro.FirstName = "Z太郎"
ztaro.Age = 0

Discussion