GoFのデザインパターンを学習する素材として、書籍「増補改訂版Java言語で学ぶデザインパターン入門」が参考になるみたいですね。
取り上げられている実例は、JAVAベースのため、Pythonで同等のプラクティスに挑んだことがありました。
Qiita記事: "Pythonで、デザインパターン「Memento」を学ぶ"
今回は、Pythonで実装した”Memento”のサンプルアプリをGolangで実装し直してみました。
■ Memento(メメント・パターン)
「Memento」という英単語は、「形見・記念」を意味します。
このパターンは、あるオブジェクトの任意の時点の状態を覚えておき(保存)、 後でその状態にオブジェクトを戻すための工夫を提供するパターンです。(カプセル化を破壊せずに、状態を元に戻せる)つまり、テキストエディタ等で実装されているような「アンドゥ」(操作をキャンセルして操作前の状態に戻す)機能を提供するためのパターンです。
注意すべきことは状態を元に戻すための必要最小限の情報(フィールド値)のみを保存すると言うことです。
(以上、「ITエンジニアのための技術支援サイト by IT専科」より引用)
UML class and sequence diagram
■ "Memento"のサンプルプログラム
実際に、Mementoパターンを活用したサンプルプログラムを動かしてみて、次のような動作の様子を確認したいと思います。なお、サンプルプログラム**「フルーツを集めていくサイコロゲーム」**は、次のような動作を想定するものとします。
- このゲームは自動的に進みます
- ゲームの主人公は、サイコロを振り、サイコロの目に応じて動作が決定します
- ゲームの都度、現在の状況を表示します(所持金、所持しているフルーツ)
- ゲームの開始時点では、所持金100円からスタート
- 現時点の所持金が、保存しておいた所持金を上回った場合は、その状況(所持金と所持している"おいしいフルーツ")を保存します
- 現時点の所持金が、保存しておいた所持金の半分を下回った場合は、以前に保存したその状況(所持金、所持している"おいしいフルーツ")を現在の状況として復元します
- お金がなくなったら終了します。
- 最大100回、ゲームを繰り返します
<サイコロの目に応じた動作>
- サイコロの目が"1"が出たとき、所持金が100円増えます
- サイコロの目が"2"が出たとき、所持金が半分になります(端数は、切り捨て)
- サイコロの目が"6"が出たとき、フルーツが貰えます
(普通の**"フルーツ"が貰えるが、"おいしいフルーツ"**が貰えるか、確率は、50%です) - その他のサイコロの目が出た場合は、何も起こりません
$ go run Main.go
==== 0
現状:[money = 100, fruits = []]
所持金が増えました
所持金は200円になりました
(だいぶ増えたので、現在の状態を保存しておこう)
==== 1
現状:[money = 200, fruits = []]
何も起こりませんでした
所持金は200円になりました
==== 2
現状:[money = 200, fruits = []]
何も起こりませんでした
所持金は200円になりました
==== 3
現状:[money = 200, fruits = []]
所持金が増えました
所持金は300円になりました
(だいぶ増えたので、現在の状態を保存しておこう)
==== 4
現状:[money = 300, fruits = []]
フルーツ(リンゴ)をもらいました
所持金は300円になりました
==== 5
現状:[money = 300, fruits = [リンゴ]]
何も起こりませんでした
所持金は300円になりました
...(snip)
==== 33
現状:[money = 600, fruits = [おいしいみかん おいしいぶどう ぶどう おいしいぶどう]]
所持金が増えました
所持金は700円になりました
(だいぶ増えたので、現在の状態を保存しておこう)
==== 34
現状:[money = 700, fruits = [おいしいみかん おいしいぶどう ぶどう おいしいぶどう]]
所持金が半分になりました
所持金は350円になりました
==== 35
現状:[money = 350, fruits = [おいしいみかん おいしいぶどう ぶどう おいしいぶどう]]
フルーツ(バナナ)をもらいました
所持金は350円になりました
==== 36
現状:[money = 350, fruits = [おいしいみかん おいしいぶどう ぶどう おいしいぶどう バナナ]]
所持金が増えました
所持金は450円になりました
==== 37
現状:[money = 450, fruits = [おいしいみかん おいしいぶどう ぶどう おいしいぶどう バナナ]]
所持金が増えました
所持金は550円になりました
==== 38
現状:[money = 550, fruits = [おいしいみかん おいしいぶどう ぶどう おいしいぶどう バナナ]]
所持金が増えました
所持金は650円になりました
==== 39
現状:[money = 650, fruits = [おいしいみかん おいしいぶどう ぶどう おいしいぶどう バナナ]]
所持金が半分になりました
所持金は325円になりました
(だいぶ減ったので、以前の状態に復帰しよう)
==== 40
現状:[money = 700, fruits = [おいしいみかん おいしいぶどう おいしいぶどう]]
所持金が増えました
所持金は800円になりました
(だいぶ増えたので、現在の状態を保存しておこう)
...(snip)
最後の方で、Memento
パターンを使った動作が確認できました。
■ サンプルプログラムの詳細
Gitリポジトリにも、同様のコードをアップしています。
https://github.com/ttsubo/study_of_design_pattern_with_golang/tree/master/Memento
- ディレクトリ構成
.
├── Main.go
└── memento
├── game.go
└── memento.go
(1) Originator(作成者)の役
Originator
役は、自分の現在の状態を保存したいときに、Memento
役を作ります。Originator
役はまた、以前のMemento
役を渡されると、そのMemento
役を作った時点の状態に戻る処理を行います。
サンプルプログラムでは、Gamer
構造体が、この役を努めます。
package memento
import (
"fmt"
"math/rand"
"strings"
"time"
)
// Gamer is struct
type Gamer struct {
fruitname, fruits []string
money int
}
// NewGamer func for initializing Game
func NewGamer(money int) *Gamer {
return &Gamer{
fruitname: []string{"リンゴ", "ぶどう", "バナナ", "みかん"},
money: money,
}
}
// GetMoney func for fetching money in Gamer
func (g *Gamer) GetMoney() int {
return g.money
}
// Bet func for betting
func (g *Gamer) Bet() {
rand.Seed(time.Now().UnixNano())
dice := rand.Intn(6) + 1
if dice == 1 {
g.money += 100
fmt.Println("所持金が増えました")
} else if dice == 2 {
g.money /= 2
fmt.Println("所持金が半分になりました")
} else if dice == 6 {
f := g.getFruit()
fmt.Printf("フルーツ(%s)をもらいました\n", f)
g.fruits = append(g.fruits, f)
} else {
fmt.Println("何も起こりませんでした")
}
}
// CreateMemento func for creating Memento
func (g *Gamer) CreateMemento() *Memento {
m := &Memento{money: g.money}
for _, f := range g.fruits {
if strings.HasPrefix(f, "おいしい") {
m.addFruit(f)
}
}
return m
}
// RestoreMemento func for restoring from Memento
func (g *Gamer) RestoreMemento(memento *Memento) {
g.money = memento.money
g.fruits = memento.GetFruits()
}
// Print func for printing current value in Gamer
func (g *Gamer) Print() string {
return fmt.Sprintf("[money = %d, fruits = %s]", g.money, g.fruits)
}
func (g *Gamer) getFruit() string {
prefix := ""
if rand.Int()%2 == 0 {
prefix = "おいしい"
}
return prefix + g.fruitname[rand.Intn(len(g.fruitname))]
}
(2) Memento(記念品)の役
Memento
役は、Originator
役の内部情報をまとめます。Memento
役は、Originator
役の内部情報を持っていますが、その情報を誰にでも公開するわけではありません。
サンプルプログラムでは、Memento
構造体が、この役を努めます。
package memento
// Memento is struct
type Memento struct {
money int
fruits []string
}
// GetMoney func for fetching money in Memento
func (m *Memento) GetMoney() int {
return m.money
}
func (m *Memento) addFruit(fruit string) {
m.fruits = append(m.fruits, fruit)
}
// GetFruits func for fetching current fruits list in Memento
func (m *Memento) GetFruits() []string {
return m.fruits
}
(3) Caretaker(世話をする人)の役
Caretaker
役は、現在のOriginator
役の状態を保存したいときに、そのことをOriginator
役に伝えます。Originator
役は、それを受けてMemento
役を作り、Caretaker
役に渡します。
Caretaker
役は将来の必要に備えて、そのMemento
役を保存しておきます。
サンプルプログラムでは、startMain
関数が、この役を努めます。
package main
import (
"fmt"
"time"
"./memento"
)
func startMain() {
gamer := memento.NewGamer(100)
memento := gamer.CreateMemento()
for i := 0; i < 100; i++ {
fmt.Printf("==== %d\n", i)
fmt.Printf("現状:%s\n", gamer.Print())
gamer.Bet()
fmt.Printf("所持金は%d円になりました\n", gamer.GetMoney())
if gamer.GetMoney() > memento.GetMoney() {
fmt.Println(" (だいぶ増えたので、現在の状態を保存しておこう)")
memento = gamer.CreateMemento()
} else if gamer.GetMoney() < memento.GetMoney()/2 {
fmt.Println(" (だいぶ減ったので、以前の状態に復帰しよう)")
gamer.RestoreMemento(memento)
}
time.Sleep(time.Second * 1)
fmt.Println("")
}
}
func main() {
startMain()
}