LoginSignup
1
2

More than 3 years have passed since last update.

Golangで、デザインパターン「Memento」を学ぶ

Posted at

GoFのデザインパターンを学習する素材として、書籍「増補改訂版Java言語で学ぶデザインパターン入門」が参考になるみたいですね。
取り上げられている実例は、JAVAベースのため、Pythonで同等のプラクティスに挑んだことがありました。
Qiita記事: "Pythonで、デザインパターン「Memento」を学ぶ"

今回は、Pythonで実装した”Memento”のサンプルアプリをGolangで実装し直してみました。

■ Memento(メメント・パターン)

「Memento」という英単語は、「形見・記念」を意味します。
このパターンは、あるオブジェクトの任意の時点の状態を覚えておき(保存)、 後でその状態にオブジェクトを戻すための工夫を提供するパターンです。(カプセル化を破壊せずに、状態を元に戻せる)つまり、テキストエディタ等で実装されているような「アンドゥ」(操作をキャンセルして操作前の状態に戻す)機能を提供するためのパターンです。
注意すべきことは状態を元に戻すための必要最小限の情報(フィールド値)のみを保存すると言うことです。
(以上、「ITエンジニアのための技術支援サイト by IT専科」より引用)

UML class and sequence diagram

W3sDesign_Memento_Design_Pattern_UML.jpg
(以上、ウィキペディア(Wikipedia)より引用)

■ "Memento"のサンプルプログラム

実際に、Mementoパターンを活用したサンプルプログラムを動かしてみて、次のような動作の様子を確認したいと思います。なお、サンプルプログラム「フルーツを集めていくサイコロゲーム」は、次のような動作を想定するものとします。

  • このゲームは自動的に進みます
  • ゲームの主人公は、サイコロを振り、サイコロの目に応じて動作が決定します
  • ゲームの都度、現在の状況を表示します(所持金、所持しているフルーツ)
  • ゲームの開始時点では、所持金100円からスタート
  • 現時点の所持金が、保存しておいた所持金を上回った場合は、その状況(所持金と所持している"おいしいフルーツ")を保存します
  • 現時点の所持金が、保存しておいた所持金の半分を下回った場合は、以前に保存したその状況(所持金、所持している"おいしいフルーツ")を現在の状況として復元します
  • お金がなくなったら終了します。
  • 最大100回、ゲームを繰り返します

<サイコロの目に応じた動作>
1. サイコロの目が"1"が出たとき、所持金が100円増えます
2. サイコロの目が"2"が出たとき、所持金が半分になります(端数は、切り捨て)
3. サイコロの目が"6"が出たとき、フルーツが貰えます
 (普通の"フルーツ"が貰えるが、"おいしいフルーツ"が貰えるか、確率は、50%です)
4. その他のサイコロの目が出た場合は、何も起こりません

$ 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構造体が、この役を努めます。

memento/game.go
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構造体が、この役を努めます。

memento/memento.go
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関数が、この役を努めます。

Main.go
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()
}

■ 参考URL

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2