LoginSignup
0
7

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-01-29

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

■ 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. その他のサイコロの目が出た場合は、何も起こりません

$ python Main.py 
==== 0
現状:[money = 100, fruits = []]
所持金が増えました
所持金は200円になりました
      (だいぶ増えたので、現在の状態を保存しておこう)

==== 1
現状:[money = 200, fruits = []]
フルーツ(おいしいぶどう)をもらいました
所持金は200円になりました

==== 2
現状:[money = 200, fruits = ['おいしいぶどう']]
何も起こりませんでした
所持金は200円になりました

==== 3
現状:[money = 200, fruits = ['おいしいぶどう']]
所持金が増えました
所持金は300円になりました
      (だいぶ増えたので、現在の状態を保存しておこう)


...(snip)


==== 22
現状:[money = 500, fruits = ['おいしいぶどう', 'おいしいぶどう', 'おいしいバナナ', 'おいしいリンゴ', 'リンゴ', 'バナナ']]
フルーツ(おいしいバナナ)をもらいました
所持金は500円になりました

==== 23
現状:[money = 500, fruits = ['おいしいぶどう', 'おいしいぶどう', 'おいしいバナナ', 'おいしいリンゴ', 'リンゴ', 'バナナ', 'おいしいバナナ']]
所持金が増えました
所持金は600円になりました
      (だいぶ増えたので、現在の状態を保存しておこう)

==== 24
現状:[money = 600, fruits = ['おいしいぶどう', 'おいしいぶどう', 'おいしいバナナ', 'おいしいリンゴ', 'リンゴ', 'バナナ', 'おいしいバナナ']]
所持金が半分になりました
所持金は300円になりました

==== 25
現状:[money = 300, fruits = ['おいしいぶどう', 'おいしいぶどう', 'おいしいバナナ', 'おいしいリンゴ', 'リンゴ', 'バナナ', 'おいしいバナナ']]
何も起こりませんでした
所持金は300円になりました

==== 26
現状:[money = 300, fruits = ['おいしいぶどう', 'おいしいぶどう', 'おいしいバナナ', 'おいしいリンゴ', 'リンゴ', 'バナナ', 'おいしいバナナ']]
所持金が半分になりました
所持金は150円になりました
      (だいぶ減ったので、以前の状態に復帰しよう)

==== 27
現状:[money = 600, fruits = ['おいしいぶどう', 'おいしいぶどう', 'おいしいバナナ', 'おいしいリンゴ', 'おいしいバナナ']]
所持金が半分になりました
所持金は300円になりました

...(snip)

最後の方で、Mementoパターンを使った動作が確認できました。

■ サンプルプログラムの詳細

Gitリポジトリにも、同様のコードをアップしています。
https://github.com/ttsubo/study_of_design_pattern/tree/master/Memento

  • ディレクトリ構成
.
├── Main.py
└── memento
    ├── __init__.py
    ├── gamer.py
    └── memento.py

(1) Originator(作成者)の役

Originator役は、自分の現在の状態を保存したいときに、Memento役を作ります。Originator役はまた、以前のMemento役を渡されると、そのMemento役を作った時点の状態に戻る処理を行います。
サンプルプログラムでは、Gamerクラスが、この役を努めます。

memento/gamer.py
import random
from memento.memento import Memento

class Gamer(object):
    def __init__(self, money):
        self.__fruitname = ["リンゴ", "ぶどう", "バナナ", "みかん"]
        self.__money = money
        self.__fruits = []

    def getMoney(self):
        return self.__money

    def bet(self):
        dice = random.randint(1, 6)
        if dice == 1:
            self.__money += 100
            print("所持金が増えました")
        elif dice == 2:
            self.__money //= 2
            print("所持金が半分になりました")
        elif dice == 6:
            f = self.__getFruit()
            print("フルーツ({0})をもらいました".format(f))
            self.__fruits.append(f)
        else:
            print("何も起こりませんでした")

    def createMemento(self):
        m = Memento(self.__money)
        for f in self.__fruits:
            if f.startswith("おいしい"):
                m.addFruit(f)
        return m

    def restoreMemento(self, memento):
        self.__money = memento.money
        self.__fruits = memento.getFruits()

    def __str__(self):
        return "[money = {0}, fruits = {1}]".format(self.__money, self.__fruits)

    def __getFruit(self):
        prefix = ''
        if bool(random.getrandbits(1)):
            prefix = "おいしい"
        return prefix + random.choice(self.__fruitname)

(2) Memento(記念品)の役

Memento役は、Originator役の内部情報をまとめます。Memento役は、Originator役の内部情報を持っていますが、その情報を誰にでも公開するわけではありません。
サンプルプログラムでは、Mementoクラスが、この役を努めます。

memento/memento.py
class Memento(object):
    def __init__(self, money):
        self.money = money
        self.fruits = []

    def getMoney(self):
        return self.money

    def addFruit(self, fruit):
        self.fruits.append(fruit)

    def getFruits(self):
        return self.fruits

(3) Caretaker(世話をする人)の役

Caretaker役は、現在のOriginator役の状態を保存したいときに、そのことをOriginator役に伝えます。Originator役は、それを受けてMemento役を作り、Caretaker役に渡します。
Caretaker役は将来の必要に備えて、そのMemento役を保存しておきます。
サンプルプログラムでは、startMainメソッドが、この役を努めます。

Main.py
import time
from memento.gamer import Gamer

def startMain():
    gamer = Gamer(100)
    memento = gamer.createMemento()

    for i in range(100):
        print("==== {0}".format(i))
        print("現状:{0}".format(gamer))
        gamer.bet()
        print("所持金は{0}円になりました".format(gamer.getMoney()))

        if gamer.getMoney() > memento.getMoney():
            print("      (だいぶ増えたので、現在の状態を保存しておこう)")
            memento = gamer.createMemento()
        elif gamer.getMoney() < memento.getMoney() / 2:
            print("      (だいぶ減ったので、以前の状態に復帰しよう)")
            gamer.restoreMemento(memento)

        time.sleep(1)
        print("")

if __name__ == '__main__':
    startMain()

■ 参考URL

0
7
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
0
7