Update 2023.11.19 2017.05.05

Python デザインパターン サンプルコード Memento
結城 浩「Java言語で学ぶデザインパターン入門」をPython化
Python3(3.11)で動くソースコード(.pyファイル .ipynbファイル)あります
「anaconda3」on .py「PyCharm」.ipynb「Jupyter Notebook」

◆◆Memento パターンの使われる場面◆◆

memento とは,記念品や遺品という意味だそうです。Memento パターンは処理や動作など状態の進行を保持し必要なときに復元する方法を提供します。

GoF の C++ サンプルは図形エディターにおいて操作を取り消す方法です。GoF 本には動機として次のように書かれています。
「オブジェクトの内部状態を記録しておく必要がある場合がある。これは,チェックポイントや取り消しメカニズム(ユーザが試しに実行したオペレーションを取り消すことを可能にしたり,エラーからの復帰に使用する)を実装する際に必要になる。オブジェクトを以前の状態に戻すことができるようにするためには,状態に関する情報をどこかに保存しておかなければならない。しかし普通,オブジェクトは自身の状態の一部,またはすべてをカプセル化しているので,他のオブジェクトからこれにアクセスすることはできないし,また外部に保存することもできない。この状態を公開することは,カプセル化の原則を破り,アプリケーションの信頼性と拡張可能性を低めることになり得る。」

操作を取り消して元に戻すということはそう簡単ではないということらしい。この方法を提供するのが Memento パターンです。

Java サンプルは実用的でないゲームの例です。ゲームの途中で気にいらないと元に戻すというちょっと乱暴な場面が出てきます。しかし,Memento パターンの例としてはこのようなものでも十分ということなのでしょう。


(2023-11-19)Python3.11で動作確認済み


◆◆Memento パターンとは◆◆

GoFによれば,Memento パターンの目的は, 「カプセル化を破壊せずに,オブジェクトの内部状態を捉えて外面化しておき,オブジェクトを後にこの状態に戻すことができるようにする。」

GoFによれば,Memento パターンの別名は,Token です。

GoFによれば,次の2点がなり立つ場合にMemento パターンを使う。
・オブジェクトの状態(の一部)のスナップショットを,後にオブジェクトをその状態に戻すことができるように,セーブしておかなければならない場合。
・状態を得るための直接的なインタフェースが,実装の詳細を公開し,オブジェクトのカプセル化を破壊する場合。

GoFによれば,Memento パターンの関連するパターンは次のようなものである。
Command パターン:command は,取り消し可能なオペレーションのために状態を保存しておくのに memento を使うことができる。
Iterator パターン:前に述べたように,memento を iteration のために使うことができる。

◆◆Memento パターンのサンプルのクラス構成◆◆

ソースコードは巻末にあり,ソースファイルはこのWebの左上隅にありダウンロードできます。
              Memento                               Gamer
(getMoney, addFruit, getFruits)  (getMoney, bet, createMemento, restoreMemento, getFruit)


class Memento は,ゲームの主人公 Gamer の状態を保持します。
class Gamer は,ゲームの主人公です。
このサンプルでは,メインルーチンが状態の保持と復元を Gamer に支持します。

◆◆Memento パターンの実装◆◆

Memento パターンでは,難しいプログラミング手法は使っていません。Java サンプルではアクセス制限を使ってクラスのカプセル化とアクセス許可を調整しています。Python にはアクセス制限がないので,class Memento とclass Gamer を独立のモジュールに入れることを推奨します。

ゲームのルールの詳細は,メソッド bet を見てください。サイコロの目によって所持金が増えたり減ったりします。

class Memento のメソッドのうち,ソースコードに narrow interface のコメントの入っているものだけメインルーチンからアクセスできます(Python では言語仕様ではない)。

class Memento とclass Gamer のフィールドは同じものです。Memento パターンのポイントは,class Gamer のメソッド createMemento, restoreMemento です。メソッド createMemento は,現在の状態を保存し,メソッド restoreMemento は,過去の状態を復元することができます。

この2つのメソッドはメインルーチンから呼ばれます。そのきっかけは Gamer(現在の状態)と Mememnt(保存された状態)の所持金を比較して,現状がある程度高ければ保存がよばれ,現状がある程度低ければ復元が呼ばれます(この仕様は任意)。

つまり,メインルーチンは Gamer に支持するだけで Gamer が Memento にアクセスして保存や復元をするのです。これらの処理に難しい手法は一切使われていません。保存するとき"おいしい"フルーツだけ選ぶというのは任意の仕様です。

class Memento のフィールド(memento.__dict__)を print するのをコメントアウトしてありますので,外してデバッグに使ってください。本来,Memento パターンは,フィールドの deepcopy だけで実装できるはずです。

最後に,乱数発生の seed は,デバッグのために同じ乱数を発生させているのですが,いろいろな結果を得るにはコメントアウトしてください。

◆◆ソースコード◆◆

このWebページの左上隅からダウンロードできます。

ソースファイルは1つです。
・Memento;Interpreter パターンのPythonサンプル

【Memento】
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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import copy
import random
import time

class Memento(object):
    def __init__(self, money):          # コンストラクタ(wide interface)
        self.money = money              # 所持金
        self.fruits = []                # フルーツ
    def getMoney(self):                 # 所持金の値を得る(narrow interface)
        return self.money
    def addFruit(self, fruit):          # フルーツを追加する(wide interface)
        self.fruits.append(fruit)
    def getFruits(self):                # フルーツを得る(wide interface)
        return copy.deepcopy(self.fruits)


class Gamer(object):
    fruitsname = ["リンゴ", "ぶどう", "バナナ", "みかん"] # フルーツ名の表
    def __init__(self, money):          # コンストラクタ
        self.money = money              # 所持金
        self.fruits = []                # フルーツ
        # random.seed(314)
    def getMoney(self):                 # 現在の所持金の値を得る
        return self.money
    def bet(self):                      # 賭ける…ゲームの進行
        dice = random.randint(1, 6)     # サイコロを振る
        if dice == 1:                   # 1の目…所持金が増える
            self.money += 100
            print("所持金が増えました")
        elif dice == 2:
            self.money /= 2             # 2の目…所持金が半分になる
            print("所持金が半分になりました")
        elif dice == 6:                 # 6の目…フルーツをもらう
            f = self.getFruit()
            print("フルーツ({})をもらいました".format(f))
            self.fruits.append(f)
        else:                           # それ以外…何も起きない
            print("何も起こりませんでした。")
    def createMemento(self):            # スナップショットをとる
        m = Memento(self.money)
        it = iter(self.fruits)
        for f in it:
            if f.startswith("おいしい"): # 文字列の先頭を調べる
                m.addFruit(f) # フルーツはおいしいものだけ保存
        return m
    def restoreMemento(self, memento):  # アンドゥを行う
        self.money = memento.getMoney()
        self.fruits = memento.getFruits()
        # print(memento.__dict__)         # debug
    def __str__(self):                  # インスタンスがprintされると呼ばれる
        return "[money = {}, fruits = {}]".format(self.money, self.fruits)
    def getFruit(self):                 # フルーツを1個得る
        prefix = ""
        if bool(random.getrandbits(1)): # 1bitの乱数
            prefix = "おいしい"
        return prefix + random.choice(Gamer.fruitsname)

def main():
    gamer = Gamer(100)                  # 最初の所持金は100
    memento = gamer.createMemento()     # 最初の状態を保存しておく
    # print(memento.__dict__)             # debug
    for i in range(50):                 # ゲームの回数
        print ("==== {}".format(i))     # 回数表示
        print("現状:{}".format(gamer))  # 現在の主人公の状態表示
        gamer.bet()
        print("所持金は{}円になりました。".format(gamer.getMoney()))
        if gamer.getMoney() > memento.getMoney():
            print("    (だいぶ増えたので、現在の状態を保存しておこう)")
            memento = gamer.createMemento()
            # print(memento.__dict__)     # debug
        elif gamer.getMoney() < memento.getMoney() / 2:
            print("    (だいぶ減ったので、以前の状態に復帰しよう)")
            gamer.restoreMemento(memento)
        time.sleep(0)
        print()

if __name__ == '__main__':
    main()
"""
==== 0
現状:[money = 100, fruits = []]
何も起こりませんでした。
所持金は100円になりました。

==== 1
現状:[money = 100, fruits = []]
フルーツ(おいしいリンゴ)をもらいました
所持金は100円になりました。

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

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

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

==== 5
現状:[money = 200, fruits = ['おいしいリンゴ', 'バナナ']]
何も起こりませんでした。
所持金は200円になりました。
"""

以上

トップページに戻る
inserted by FC2 system