こんにちは、KOUKIです。
この記事では、デザインパターンの一つであるPrototypeパターンについて、紹介しています。
Prototypeパターンは、インスタンスのコピーや使い回しに関係するパターンです。
<目次>
デザインパターンまとめ
Deep Copy
インスタンスをコピーする方法を学びましょう。
シチュエーション
インスタンスのコピーには、細心の注意を払うべきです。プロパティがポインタを持っている場合は、特にです。
iPhone構造体を定義します。
1 2 3 4 5 6 7 8 9 10 11 |
package main type Feature struct { Width float64 Volume string } type iPhone struct { Name string Feature *Feature } |
この構造体からiPhoneXインスタンスを作成します。
1 2 3 4 5 6 |
func main() { iphoneX := iPhone{"iPhoneX", &Feature{5.8, "64/256GB"}} fmt.Println(iphoneX, iphoneX.Feature) } |
1 2 |
$ go run main.go {iPhoneX 0xc00000c060} &{5.8 64/256GB} |
うまくいきましたね。
次に、iPhoneXからiPhone11を作成しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
func main() { iphoneX := iPhone{"iPhoneX", &Feature{5.8, "64/256GB"}} // iphoneXからiphone11を作成する iphone11 := iphoneX // パラメータを変える iphone11.Name = "iPhone11" iphone11.Feature.Width = 6.1 iphone11.Feature.Volume = "64/128/256GB" fmt.Println(iphoneX, iphoneX.Feature) fmt.Println(iphone11, iphone11.Feature) } |
簡単ですね…と言いたいところですが、この実装には以下のバグが発生します。
1 2 3 |
$ go run main.go {iPhoneX 0xc00000c080} &{6.1 64/128/256GB} <<<< Featureが!!! {iPhone11 0xc00000c080} &{6.1 64/128/256GB} |
iphoneXのFeatureがiphone11のFeatureに書き換わっています。
Featureプロパティはポインタ(*)を使っているので、コピーした時に、コピー元と同じアドレスがiphone11にも渡されるので、値が上書きされてしまいます。
このようなバグは、現場でも発生します。そして、重大なバグになりやすいです。
新しいアドレスをセットする
正しくコピーするためには、コピー後に新しいアドレスで上書きします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
iphoneX := iPhone{"iPhoneX", &Feature{5.8, "64/256GB"}} iphone11 := iphoneX // 新しいアドレスで上書き iphone11.Feature = &Feature{ iphoneX.Feature.Width, iphoneX.Feature.Volume} // プロパティを設定する iphone11.Name = "iPhone11" iphone11.Feature.Width = 6.1 iphone11.Feature.Volume = "64/128/256GB" fmt.Println(iphoneX, iphoneX.Feature) fmt.Println(iphone11, iphone11.Feature) } |
1 2 3 |
$ go run main.go {iPhoneX 0xc00000c080} &{5.8 64/256GB} {iPhone11 0xc00000c0a0} &{6.1 64/128/256GB} |
OKですね。
しかし、ちょっとわかりずらいですよね。
メソッドにする
先ほどのサンプルをメソッドを使った実装に書き換えてみましょう。
まず、コピー用のメソッドを用意します。
1 2 3 4 5 |
func (f *Feature) DeepCopy() *Feature { return &Feature{ f.Width, f.Volume} } |
Featureプロパティをこのメソッドを使って設定しましょう。
1 2 3 4 5 6 7 8 9 10 |
func main() { ... // DeepCopy版に書き換える iphone11.Feature = iphoneX.Feature.DeepCopy() // iphone11.Feature = &Feature{ // iphoneX.Feature.Width, // iphoneX.Feature.Volume} ... } |
いくらかスッキリしましたね。
1 2 3 |
$ go run main.go {iPhoneX 0xc0000a6020} &{5.8 64/256GB} {iPhone11 0xc0000a6040} &{6.1 64/128/256GB} |
スライスのコピーにも注意が必要
インスタンスと同様にスライスをコピーする時も気をつけなければなりません。
例えば、こんな処理があったとします。
1 2 3 4 5 6 7 |
func main() { s1 := []string{"one", "two"} s2 := s1 s2[0] = "three" fmt.Println("s1 = ", s1) fmt.Println("s2 = ", s2) } |
string型のスライスを用意しました。
s2にはs1の情報をコピーしており、「s2[0]=”three”」で元々の値を書き換えています。
この場合、結果は次のようになります。
1 2 3 |
$ go run main.go s1 = [three two] <<<< s1に影響が出てる! s2 = [three two] |
なんとs1の値まで書き変わってしまうのです!
スライスは、参照渡し(アドレスを渡す)で処理されるので上記のようになります。そして、これはバグを発生させます。
そのため、スライスを含むインスタンスをコピーする用途がある場合、DeepCopyメソッドを用意した方が良いと思います。
例えば、iPhoneにアプリケーション保存領域を追加しましょう。
1 2 3 4 5 |
type iPhone struct { Name string Feature *Feature Apps []string // Applicationプロパティを追加 } |
次に、iPhoneにDeepCopyメソッドを用意します。
1 2 3 4 5 6 7 8 |
func (i *iPhone) DeepCopy() *iPhone { newIPhone := *i // Featureを新しいアドレスでコピー newIPhone.Feature = i.Feature.DeepCopy() // Appsを新しいアドレスでコピー copy(newIPhone.Apps, i.Apps) return &newIPhone } |
ここでは、ついでにFeatureプロパティのDeepCopyも呼び出しました。
copy関数はGo言語に最初から組み込まれているもので、スライスをコピーすることができます。
早速、使ってみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
func main() { iphoneX := iPhone{"iPhoneX", &Feature{5.8, "64/256GB"}, []string{"google map"}, } // DeepCopyでインスタンスをコピー iphone11 := iphoneX.DeepCopy() // プロパティ設定 iphone11.Name = "iPhone11" iphone11.Feature.Width = 6.1 iphone11.Feature.Volume = "64/128/256GB" iphone11.Apps = append(iphone11.Apps, "YouTube") fmt.Println(iphoneX, iphoneX.Feature) fmt.Println(iphone11, iphone11.Feature) } |
1 2 3 |
$ go run main.go {iPhoneX 0xc00000c080 [google map]} &{5.8 64/256GB} &{iPhone11 0xc00000c0a0 [google map YouTube]} &{6.1 64/128/256GB} |
iPhonXのAppsが「[google map]」、iPhone11のAppsが「[google map YouTube]」になっているので、Deep Copyは成功ですね。
シリアライズ化(Encode/Decode)
先ほどは、インスタンスのアドレスを書き換えることで、DeepCopyを実現しました。それ以外にもシリアライズ化(Encode/Decode)をすることで、同様の処理を実装することも可能です。
1 2 3 4 5 6 7 8 9 10 11 12 |
func (i *iPhone) DeepCopy() *iPhone { b := bytes.Buffer{} e := gob.NewEncoder(&b) _ = e.Encode(i) d := gob.NewDecoder(&b) newIPhone := iPhone{} _ = d.Decode(&newIPhone) return &newIPhone } |
呼び出し側の変更は、不要です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
func main() { iphoneX := iPhone{"iPhoneX", &Feature{5.8, "64/256GB"}, []string{"google map"}, } iphone11 := iphoneX.DeepCopy() iphone11.Name = "iPhone11" iphone11.Feature.Width = 6.1 iphone11.Feature.Volume = "64/128/256GB" iphone11.Apps = append(iphone11.Apps, "YouTube") fmt.Println(iphoneX, iphoneX.Feature) fmt.Println(iphone11, iphone11.Feature) } |
1 2 3 |
$ go run main.go {iPhoneX 0xc00000c860 [google map]} &{5.8 64/256GB} &{iPhone11 0xc00000cf60 [google map YouTube]} &{6.1 64/128/256GB} |
プロトタイプ関数
iphoneXやiphon11のパラメータは手動で設定しましたが、これを関数で設定できるようにしましょう。
最初に、iPhoneXとiPhone11の基本パラメータを変数で定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
package main import ( "bytes" "encoding/gob" ) type Feature struct { Width float64 Volume int } type iPhone struct { Name string Feature *Feature } // プロトタイプ var ( iPhoneX = iPhone{"", &Feature{5.8, 0}} iPhone11 = iPhone{"", &Feature{6.1, 0}} ) |
名前(Name)と容量(Volume)を自分で設定できるようにしました。
次に、DeepCopyメソッドを用意します。
1 2 3 4 5 6 7 8 9 10 11 12 |
func (i *iPhone) DeepCopy() *iPhone { b := bytes.Buffer{} e := gob.NewEncoder(&b) _ = e.Encode(i) d := gob.NewDecoder(&b) newIPhone := iPhone{} _ = d.Decode(&newIPhone) return &newIPhone } |
続いて、コンストラクタ関数を定義します。
1 2 3 4 5 6 7 |
// コンストラクタ(内部用) func newIPhone(proto *iPhone, name string, volume int) *iPhone { newIPhone := proto.DeepCopy() newIPhone.Name = name newIPhone.Feature.Volume = volume return newIPhone } |
このコンストラクタ関数は内部用なので、プロトタイプ関数から呼び出します。
1 2 3 4 5 6 7 8 9 |
func NewIPhoneX(name string, volume int) *iPhone { // プロトタイプを渡すことで生成できるインスタンスを制御 return newIPhone(&iPhoneX, name, volume) } func NewIPhone11(name string, volume int) *iPhone { // プロトタイプを渡すことで生成できるインスタンスを制御 return newIPhone(&iPhone11, name, volume) } |
newIPhone関数に渡すプロトタイプによって、生成するインスタンスを制御しています。
ここまでで、実装は完了です。早速使ってみましょう。
1 2 3 4 5 6 |
func main() { myIphoneX := NewIPhoneX("my iPhoneX", 128) myIphone11 := NewIPhone11("my iPhone11", 256) fmt.Println(myIphoneX, myIphoneX.Feature) fmt.Println(myIphone11, myIphone11.Feature) } |
今までの実装と比べて、呼び出し処理がシンプルになりましたね。
1 2 3 |
$ go run main.go &{my iPhoneX 0xc0000b4410} &{5.8 128} &{my iPhone11 0xc0000b4580} &{5.8 256} |
まとめ
デザインパターンを学ぶとプログラミングの実装が、うまくなっていく感覚があります。
それは、今まで複雑に組んでしまっていたコードを、よりシンプルに実装できるようになったからだと思います。
何でもかんでもパターンに当てはめる、ということをするつもりはありませんが、実装の過程で「このパターンで実装したら面白そう!」など出てくると思うので、その場合は、使っていこうと思います。
それでは、また!
Go言語まとめ
ソースコード
Deep Copying
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
package main import "fmt" type Feature struct { Width float64 Volume string } type iPhone struct { Name string Feature *Feature } func main() { iphoneX := iPhone{"iPhoneX", &Feature{5.8, "64/256GB"}} iphone11 := iphoneX // 新しいアドレスで上書き iphone11.Feature = &Feature{ iphoneX.Feature.Width, iphoneX.Feature.Volume} // プロパティを設定する iphone11.Name = "iPhone11" iphone11.Feature.Width = 6.1 iphone11.Feature.Volume = "64/128/256GB" fmt.Println(iphoneX, iphoneX.Feature) fmt.Println(iphone11, iphone11.Feature) } |
Deep Copy Method
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
package main import "fmt" type Feature struct { Width float64 Volume string } func (f *Feature) DeepCopy() *Feature { return &Feature{ f.Width, f.Volume} } type iPhone struct { Name string Feature *Feature Apps []string } func (i *iPhone) DeepCopy() *iPhone { newIPhone := *i // Featureを新しいアドレスでコピー newIPhone.Feature = i.Feature.DeepCopy() // Appsを新しいアドレスでコピー copy(newIPhone.Apps, i.Apps) return &newIPhone } func main() { iphoneX := iPhone{"iPhoneX", &Feature{5.8, "64/256GB"}, []string{"google map"}, } // DeepCopyでインスタンスをコピー iphone11 := iphoneX.DeepCopy() // プロパティ設定 iphone11.Name = "iPhone11" iphone11.Feature.Width = 6.1 iphone11.Feature.Volume = "64/128/256GB" iphone11.Apps = append(iphone11.Apps, "YouTube") fmt.Println(iphoneX, iphoneX.Feature) fmt.Println(iphone11, iphone11.Feature) } |
Encode/Decode
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
package main import ( "bytes" "encoding/gob" "fmt" ) type Feature struct { Width float64 Volume string } type iPhone struct { Name string Feature *Feature Apps []string } func (i *iPhone) DeepCopy() *iPhone { b := bytes.Buffer{} e := gob.NewEncoder(&b) _ = e.Encode(i) d := gob.NewDecoder(&b) newIPhone := iPhone{} _ = d.Decode(&newIPhone) return &newIPhone } func main() { iphoneX := iPhone{"iPhoneX", &Feature{5.8, "64/256GB"}, []string{"google map"}, } iphone11 := iphoneX.DeepCopy() iphone11.Name = "iPhone11" iphone11.Feature.Width = 6.1 iphone11.Feature.Volume = "64/128/256GB" iphone11.Apps = append(iphone11.Apps, "YouTube") fmt.Println(iphoneX, iphoneX.Feature) fmt.Println(iphone11, iphone11.Feature) } |
Prototype
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
package main import ( "bytes" "encoding/gob" "fmt" ) type Feature struct { Width float64 Volume int } type iPhone struct { Name string Feature *Feature } // プロトタイプ var ( iPhoneX = iPhone{"", &Feature{5.8, 0}} iPhone11 = iPhone{"", &Feature{6.1, 0}} ) func (i *iPhone) DeepCopy() *iPhone { b := bytes.Buffer{} e := gob.NewEncoder(&b) _ = e.Encode(i) d := gob.NewDecoder(&b) newIPhone := iPhone{} _ = d.Decode(&newIPhone) return &newIPhone } // コンストラクタ(内部用) func newIPhone(proto *iPhone, name string, volume int) *iPhone { newIPhone := proto.DeepCopy() newIPhone.Name = name newIPhone.Feature.Volume = volume return newIPhone } func NewIPhoneX(name string, volume int) *iPhone { // プロトタイプを渡すことで生成できるインスタンスを制御 return newIPhone(&iPhoneX, name, volume) } func NewIPhone11(name string, volume int) *iPhone { // プロトタイプを渡すことで生成できるインスタンスを制御 return newIPhone(&iPhone11, name, volume) } func main() { myIphoneX := NewIPhoneX("my iPhoneX", 128) myIphone11 := NewIPhoneX("my iPhone11", 256) fmt.Println(myIphoneX, myIphoneX.Feature) fmt.Println(myIphone11, myIphone11.Feature) } |
コメントを残す
コメントを投稿するにはログインしてください。