GoFのデザインパターンを学習する素材として、書籍「増補改訂版Java言語で学ぶデザインパターン入門」が参考になるみたいですね。ただ、取り上げられている実例は、JAVAベースのため、自分の理解を深めるためにも、Pythonで同等のプラクティスに挑んでみました。
■ Memento(メメント・パターン)
「Memento」という英単語は、「形見・記念」を意味します。
このパターンは、あるオブジェクトの任意の時点の状態を覚えておき(保存)、 後でその状態にオブジェクトを戻すための工夫を提供するパターンです。(カプセル化を破壊せずに、状態を元に戻せる)つまり、テキストエディタ等で実装されているような「アンドゥ」(操作をキャンセルして操作前の状態に戻す)機能を提供するためのパターンです。
注意すべきことは状態を元に戻すための必要最小限の情報(フィールド値)のみを保存すると言うことです。
(以上、「ITエンジニアのための技術支援サイト by IT専科」より引用)
UML class and sequence diagram
■ "Memento"のサンプルプログラム
実際に、Mementoパターンを活用したサンプルプログラムを動かしてみて、次のような動作の様子を確認したいと思います。なお、サンプルプログラム**「フルーツを集めていくサイコロゲーム」**は、次のような動作を想定するものとします。
- このゲームは自動的に進みます
- ゲームの主人公は、サイコロを振り、サイコロの目に応じて動作が決定します
- ゲームの都度、現在の状況を表示します(所持金、所持しているフルーツ)
- ゲームの開始時点では、所持金100円からスタート
- 現時点の所持金が、保存しておいた所持金を上回った場合は、その状況(所持金と所持している"おいしいフルーツ")を保存します
- 現時点の所持金が、保存しておいた所持金の半分を下回った場合は、以前に保存したその状況(所持金、所持している"おいしいフルーツ")を現在の状況として復元します
- お金がなくなったら終了します。
- 最大100回、ゲームを繰り返します
<サイコロの目に応じた動作>
- サイコロの目が"1"が出たとき、所持金が100円増えます
- サイコロの目が"2"が出たとき、所持金が半分になります(端数は、切り捨て)
- サイコロの目が"6"が出たとき、フルーツが貰えます
(普通の**"フルーツ"が貰えるが、"おいしいフルーツ"**が貰えるか、確率は、50%です) - その他のサイコロの目が出た場合は、何も起こりません
$ 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
クラスが、この役を努めます。
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
クラスが、この役を努めます。
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
メソッドが、この役を努めます。
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()