← 一覧に戻る
振る舞いに関するパターン

Memento

状態を保存する

概要

やり直しのスナップショットをとって置く

インスタンスを復元したいとき、アクセス修飾子をprivate以外にしないといけなくなる(しなくてもいいけど、内部構造に依存したプログラムになる。カプセル化の破壊)

ミソ

スナップショットは実態よりはるかに軽いもの なのでケアテイカーはバックアップをいくらでもスタックしておける オブジェクトそのものでなくその属性のコピーなどでも良し

マンガでわかる Memento

マンガでわかる Memento #デザインパターン - Qiita

でざぱたんで覚える Memento

ちびキャラは「メメントたん」。「忘れる」権利を失い、起きたことすべてを記憶し続ける超記憶者の少女。全部覚えている記録係がいれば、いつでもあの時点に戻せる——状態を別オブジェクトに退避して復元可能にするMementoのundoの構図そのもの。GUIの多段undoならCommandの方が柔軟、一時保存や軽いundoならMementoが手軽、という使い分けの指針も載っている。

出典: いしだけ『でざぱたん: ちびキャラで覚えるデザインパターン』(P.111〜)

登場人物

  • オリジネイター:リアルな事象。メメントを提供したり自分をメメントで更新したり
  • ケアテイカー:適切なタイミングでオリジネイターのバックアップを残す
  • まずオリジネイターに「バックアップを取る」ことを通知
  • オリジネイターはメメントを作成する
  • ケアテイカーはそのメメントを受け取り、保存する
  • 狭いインタフェースしか使えない。メメントはひとかたまりのブラックボックスとして置いておく
  • メメント:バックアップ。オリジネイターの内部情報を持つ
  • 狭いインタフェース: ケアテイカーが利用できるインタフェース
  • ここでの狭いとは「内部状態を操作できる度合いが少ない」publicなインタフェース
  • 広いインタフェース: メメントはケアテイカーやMainが作るのではなく、オリジネイター(同一パッケージ)しか作れない
  • 広いとは、内部状態をいじれるpackage privateなインタフェース

クラス図

Memento Pattern

このサイトの実装(ゲームの所持金セーブ&アンドゥ)での対応関係:

classDiagram
  class Gamer {
    -money int
    -fruits List
    -random Random
    +getMoney() int
    +bet()
    +createMemento() Memento
    +restoreMemento(memento)
  }
  class Memento {
    ~money int
    ~fruits ArrayList
    +getMoney() int
    ~Memento(money)
    ~addFruit(fruit)
    ~getFruits() List
  }
  class Main {
    +main(args)
  }
  Gamer ..> Memento : createMemento()/restoreMemento()
  Main o-- Gamer
  Main ..> Memento : 保存中のスナップショットを保持

ユースケース

Git

「スナップショット」がそのままメメントだし、オリジネイターはソースコード本体。本体のバックアップではなく圧縮ファイルを取っておいて、好きなところまで巻き戻しを行える。

Java
game/Memento.java
package game;
import java.util.*;

public class Memento {
    int money;                              // 所持金
    ArrayList fruits;                       // フルーツ
    public int getMoney() {                 // 所持金を得る(narrow interface)
        return money;
    }
    Memento(int money) {                    // コンストラクタ(wide interface)
        this.money = money;
        this.fruits = new ArrayList();
    }
    void addFruit(String fruit) {           // フルーツを追加する(wide interface)
        fruits.add(fruit);
    }
    List getFruits() {                    // フルーツを得る(wide interface)
         return (List)fruits.clone();
    }
}
game/Gamer.java
package game;
import java.util.*;

public class Gamer {
    private int money;                          // 所持金
    private List fruits = new ArrayList();      // フルーツ
    private Random random = new Random();       // 乱数発生器
    private static String[] fruitsname = {      // フルーツ名の表
        "リンゴ", "ぶどう", "バナナ", "みかん",
    };
    public Gamer(int money) {                   // コンストラクタ
        this.money = money;
    }
    public int getMoney() {                     // 現在の所持金を得る
        return money;
    }
    public void bet() {                         // 賭ける…ゲームの進行
        int dice = random.nextInt(6) + 1;           // サイコロを振る
        if (dice == 1) {                            // 1の目…所持金が増える
            money += 100;
            System.out.println("所持金が増えました。");
        } else if (dice == 2) {                     // 2の目…所持金が半分になる
            money /= 2;
            System.out.println("所持金が半分になりました。");
        } else if (dice == 6) {                     // 6の目…フルーツをもらう
            String f = getFruit();
            System.out.println("フルーツ(" + f + ")をもらいました。");
            fruits.add(f);
        } else {                                    // それ以外…何も起きない
            System.out.println("何も起こりませんでした。");
        }
    }
    public Memento createMemento() {                // スナップショットをとる
        Memento m = new Memento(money);
        Iterator it = fruits.iterator();
        while (it.hasNext()) {
            String f = (String)it.next();
            if (f.startsWith("おいしい")) {         // フルーツはおいしいものだけ保存
                m.addFruit(f);
            }
        }
        return m;
    }
    public void restoreMemento(Memento memento) {   // アンドゥを行う
        this.money = memento.money;
        this.fruits = memento.getFruits();
    }
    public String toString() {                      // 文字列表現
        return "[money = " + money + ", fruits = " + fruits + "]";
    }
    private String getFruit() {                     // フルーツを1個得る
        String prefix = "";
        if (random.nextBoolean()) {
            prefix = "おいしい";
        }
        return prefix + fruitsname[random.nextInt(fruitsname.length)];
    }
}
Main.java
import game.Memento;
import game.Gamer;

public class Main {
    public static void main(String[] args) {
        Gamer gamer = new Gamer(100);               // 最初の所持金は100
        Memento memento = gamer.createMemento();    // 最初の状態を保存しておく
        for (int i = 0; i < 100; i++) {
            System.out.println("==== " + i);        // 回数表示
            System.out.println("現状:" + gamer);    // 現在の主人公の状態表示

            gamer.bet();    // ゲームを進める

            System.out.println("所持金は" + gamer.getMoney() + "円になりました。");

            // Mementoの取り扱いの決定
            if (gamer.getMoney() > memento.getMoney()) {
                System.out.println("    (だいぶ増えたので、現在の状態を保存しておこう)");
                memento = gamer.createMemento();
            } else if (gamer.getMoney() < memento.getMoney() / 2) {
                System.out.println("    (だいぶ減ったので、以前の状態に復帰しよう)");
                gamer.restoreMemento(memento);
            }

            // 時間待ち
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
            System.out.println("");
        }
    }
}
Go

Java版と同じ題材(所持金/フルーツを持つGamerのセーブ&アンドゥ)を移植。見どころは「狭いインタフェース/広いインタフェース」の再現方法で、GoにJavaのpackage-privateに相当する修飾子は無いが、識別子を小文字始まりにするだけで得られる「同じpackage内からしか参照できない」性質(unexported)がJavaのpackage-privateとほぼそのまま対応する。Memento.Money()だけをエクスポートし(狭いインタフェース)、コンストラクタ・addFruitgetFruitsは非公開のままgameパッケージ(Gamerと同じパッケージ)に閉じ込める(広いインタフェース)。main(Caretaker役)から広いインタフェースを呼ぼうとするとコンパイルエラーになる、という強制力までJava版と対応している。

実行: go run ./GoF/patterns/Memento/go [seed]

$ go run ./GoF/patterns/Memento/go 314
game/memento.go
// Package game は Java版 Memento サンプルの `game` パッケージに対応する。
//
// Java版の Memento は「狭いインタフェース」(money取得のみ、public)と
// 「広いインタフェース」(コンストラクタ・addFruit・getFruits、package-private=
// パッケージ外からはアクセス不可)を使い分ける。
// Goにアクセス修飾子は無いが、識別子の先頭を小文字にするだけで「同じpackage内から
// しか参照できない」性質(unexported)が得られ、これがJavaのpackage-privateと
// ほぼそのまま対応する。狭いインタフェースは Money() としてエクスポートし、
// 広いインタフェースは newMemento/addFruit/getFruits として非公開のままにする。
// game パッケージの外(=main パッケージ、Caretaker役)からは Money() しか呼べず、
// それ以外を呼ぼうとするとコンパイルエラーになる。
package game

// Memento は Gamer の状態のスナップショット(バックアップ)。
type Memento struct {
	money  int      // 所持金
	fruits []string // フルーツ(おいしいものだけ保存される)
}

// newMemento はコンストラクタ相当。広いインタフェース: package外(main)からは呼べない。
func newMemento(money int) *Memento {
	return &Memento{money: money, fruits: []string{}}
}

// Money は所持金を得る。狭いインタフェース: Caretaker(main)からも呼べる。
func (m *Memento) Money() int {
	return m.money
}

// addFruit はフルーツを追加する。広いインタフェース。
func (m *Memento) addFruit(fruit string) {
	m.fruits = append(m.fruits, fruit)
}

// getFruits はフルーツのコピーを得る。広いインタフェース。
// スライスの参照をそのまま返すと呼び出し元がMementoの内部状態を書き換えられて
// しまう(Java版がArrayList#cloneしているのと同じ理由)ので、コピーを返す。
func (m *Memento) getFruits() []string {
	cp := make([]string, len(m.fruits))
	copy(cp, m.fruits)
	return cp
}
game/gamer.go
package game

import (
	"fmt"
	"math/rand"
	"strings"
)

var fruitsName = [...]string{"リンゴ", "ぶどう", "バナナ", "みかん"} // フルーツ名の表

// Gamer はゲームの主人公(Originator)。
type Gamer struct {
	money  int      // 所持金
	fruits []string // フルーツ
	random *rand.Rand
}

// NewGamer はコンストラクタ。乱数はseedから作るので、同じseedなら同じ挙動を再現できる。
func NewGamer(money int, seed int64) *Gamer {
	return &Gamer{
		money:  money,
		fruits: []string{},
		random: rand.New(rand.NewSource(seed)),
	}
}

// Money は現在の所持金を得る。
func (g *Gamer) Money() int {
	return g.money
}

// Bet は賭ける…ゲームの進行。
func (g *Gamer) Bet() {
	dice := g.random.Intn(6) + 1 // サイコロを振る
	switch dice {
	case 1: // 1の目…所持金が増える
		g.money += 100
		fmt.Println("所持金が増えました。")
	case 2: // 2の目…所持金が半分になる
		g.money /= 2
		fmt.Println("所持金が半分になりました。")
	case 6: // 6の目…フルーツをもらう
		f := g.getFruit()
		fmt.Printf("フルーツ(%s)をもらいました。\n", f)
		g.fruits = append(g.fruits, f)
	default: // それ以外…何も起きない
		fmt.Println("何も起こりませんでした。")
	}
}

// CreateMemento はスナップショットをとる。
// 同じpackage内なので、Mementoの広いインタフェース(newMemento/addFruit)を直接使える。
// main パッケージ(Caretaker)にはこのメソッド自体は公開するが、中で何を呼んでいるかは
// 気にしなくてよい――Mementoの中身をどう作るかはOriginator(Gamer)だけが知っていればいい。
func (g *Gamer) CreateMemento() *Memento {
	m := newMemento(g.money)
	for _, f := range g.fruits {
		if strings.HasPrefix(f, "おいしい") { // フルーツはおいしいものだけ保存
			m.addFruit(f)
		}
	}
	return m
}

// RestoreMemento はアンドゥを行う。
func (g *Gamer) RestoreMemento(memento *Memento) {
	g.money = memento.money // 同じpackageなので非公開フィールドに直接触れる(広いインタフェース)
	g.fruits = memento.getFruits()
}

// String は文字列表現(fmt.Stringer)。Java版のtoString()相当。
func (g *Gamer) String() string {
	return fmt.Sprintf("[money = %d, fruits = [%s]]", g.money, strings.Join(g.fruits, ", "))
}

// getFruit はフルーツを1個得る。
func (g *Gamer) getFruit() string {
	prefix := ""
	if g.random.Intn(2) == 1 { // Java の random.nextBoolean() 相当
		prefix = "おいしい"
	}
	return prefix + fruitsName[g.random.Intn(len(fruitsName))]
}
main.go
package main

import (
	"fmt"
	"os"
	"strconv"

	"github.com/sayuen0/design-pattern-study/GoF/patterns/Memento/go/game"
)

// 実行:
//
//	go run ./GoF/patterns/Memento/go        # デフォルトseed(314)
//	go run ./GoF/patterns/Memento/go 314    # seed指定
//
// Java版は演出用に毎ループ Thread.sleep(1000) を挟んで100ゲーム進めるが、
// これはパターンの本質と無関係な人間向けの間(ま)なので省略し、即座に流す。
//
// 見どころ: main(Caretaker役)は game.Memento の Money() しか呼ばない(狭いインタフェース)。
// フルーツの追加やコンストラクタ(広いインタフェース)は game パッケージ内(Gamer)からしか
// 呼べず、たとえばここで memento.addFruit(...) と書こうとするとコンパイルエラーになる。
func main() {
	seed := int64(314)
	if len(os.Args) == 2 {
		s, err := strconv.ParseInt(os.Args[1], 10, 64)
		if err != nil {
			fmt.Println("Usage: go run ./GoF/patterns/Memento/go [seed]")
			os.Exit(1)
		}
		seed = s
	}

	gamer := game.NewGamer(100, seed) // 最初の所持金は100
	memento := gamer.CreateMemento()  // 最初の状態を保存しておく

	for i := 0; i < 100; i++ {
		fmt.Printf("==== %d\n", i)   // 回数表示
		fmt.Printf("現状:%s\n", gamer) // 現在の主人公の状態表示

		gamer.Bet() // ゲームを進める

		fmt.Printf("所持金は%d円になりました。\n", gamer.Money())

		// Mementoの取り扱いの決定
		switch {
		case gamer.Money() > memento.Money():
			fmt.Println("    (だいぶ増えたので、現在の状態を保存しておこう)")
			memento = gamer.CreateMemento()
		case gamer.Money() < memento.Money()/2:
			fmt.Println("    (だいぶ減ったので、以前の状態に復帰しよう)")
			gamer.RestoreMemento(memento)
		}

		fmt.Println()
	}
}
PHP
index.php
<?php

/**
 * オブジェクトのundoを実装する
 * 
 * mementoとは「忘れ形見」
 * 処理前の情報を残しておいてそれを再現するといったところだろうか
 * Commandより平易な実装
 */

/**
 * オリジネイター:操作対象オブジェクト
 * ケアテイカー:メメントを記憶するオブジェクト
 * メメント:状態オブジェクト
 */


//  Originator

class Article
{
  private $subject = null;
  private $body = null;
  private $datetime = null;

  public function setSubject($s)
  {
    $this->subject = $s;
  }

  public function setBody($s)
  {
    $this->body = $s;
  }

  public function setDatetime($d)
  {
    $this->datetime = $d;
  }

  public function getSubject()
  {
    return $this->subject;
  }
  public function getBody()
  {
    return $this->body;
  }
  public function getDatetime()
  {
    return $this->datetime;
  }

  // memento作成
  public function createMemento()
  {
    $obj = new ArticleMemento();
    $obj->setSubject($this->getSubject());
    $obj->setBody($this->getBody());
    $obj->setDatetime($this->getDatetime());
    return $obj;
  }

  // mementoをセットしてその状態を復元
  public function setMemento($obj)
  {
    $this->setSubject($obj->getSubject());
    $this->setBody($obj->getBody());
    $this->setDatetime($obj->getDatetime());
  }
}


// memento: 記憶するやつ

class ArticleMemento
{
  private $subject = null;
  private $body = null;
  private $datetime = null;

  public function setSubject($s)
  {
    $this->subject = $s;
  }

  public function setBody($s)
  {
    $this->body = $s;
  }

  public function setDatetime($d)
  {
    $this->datetime = $d;
  }

  public function getSubject()
  {
    return $this->subject;
  }
  public function getBody()
  {
    return $this->body;
  }
  public function getDatetime()
  {
    return $this->datetime;
  }
}



// CareTaker: メメントを保持するやつ

class ArticleCaretaker
{
  private $memento = null;

  public function getMemento()
  {
    return $this->memento;
  }

  public function setMemento($memento)
  {
    $this->memento = $memento;
  }
}


// コントローラ

date_default_timezone_set("Asia/Tokyo");
function say($s)
{
  print $s . "<br>";
}


say("初期状態");

$article = new Article();
$article->setSubject("変更前の記事の件名");
$article->setBody("変更前の記事の本文");
$article->setDatetime(date("Y/m/d H:i:s"));

say($article->getSubject());
say($article->getBody());
say($article->getDatetime());

// ケアテイカーに今のArticleのメメントをセットし記憶
$act = new ArticleCareTaker();
$act->setMemento($article->createMemento());

say("変更");
$article->setSubject("変更後の記事の件名");
$article->setBody("変更後の記事の本文");
$article->setSubject(date("Y/m/d H:i:s", strtotime("+1hour")));

say($article->getSubject());
say($article->getBody());
say($article->getDatetime());


say("復元");
// Articleにさっきケアテイカーに保存したメメントをセットして復元
$article->setMemento($act->getMemento());

say($article->getSubject());
say($article->getBody());
say($article->getDatetime());
TypeScript

TypeScript/JSにはJavaのpackage-privateに相当するアクセス修飾子が無く、公開範囲の単位は「クラス」(private/protected)か「モジュール=ファイル」(exportの有無)しかない。そこでMementoとGamerを同じファイル(game.ts、Java版のgameパッケージに相当)に置き、広いインタフェースを持つ実装クラスMementoImplをexportしないことで「Gamerからは使えるが別ファイルのmain.tsからは型として拾えない」状態を作る。ただしこれはコンパイル時の型チェックによる制限にすぎず、Goのpackage境界のように実行時にも強制される訳ではない(as anyで型チェックを迂回すれば呼べてしまう)、という限界も併せて示している。乱数はNode標準にシード指定可能なものが無いため、Strategyパターンで確立した自前のseedable RNG(xorshift32)を踏襲する。

実行: npx tsx main.ts [seed]

$ npx tsx GoF/patterns/Memento/typescript/main.ts 314
game.ts
// game モジュール: Java版 Memento サンプルの `game` パッケージに対応する。
//
// Java版のMementoは「狭いインタフェース」(money取得のみ、public)と「広いインタフェース」
// (コンストラクタ・addFruit・getFruits、package-private=gameパッケージ内、つまり
// Gamerからしかアクセスできない)を使い分けている。
//
// TypeScript/JSにはJavaのpackage-privateに相当するアクセス修飾子が無く、公開範囲の単位は
// 「クラス」(private/protected)か「モジュール=ファイル」(exportの有無)しかない。
// ここではMementoとGamerを同じファイル(このgame.ts、Java版のgameパッケージに相当)に
// 置き、広いインタフェースを持つ実装クラス MementoImpl をexportしないことで、
// 「Gamerからは使えるが、別ファイルのmain.tsからは型として拾えない」状態を作る。
// main.tsがimportできるのはexportされた狭いインタフェース(Memento型)とGamerだけ。
//
// 注意: これはあくまでコンパイル時の型チェックによる制限。Goのpackage境界のように
// 実行時にも強制される訳ではない。main.ts側で `(memento as any).addFruit(...)` と書けば
// 型チェックを迂回して広いインタフェースを直接呼べてしまう(構造的型付け言語ゆえの限界)。

// 乱数: Node標準にはシード指定可能な乱数が無い(Math.randomはシード不可)。
// Strategyパターンで確立した方法を踏襲し、xorshift32による自前のseedable RNGを使う。
class SeededRandom {
  private state: number;
  constructor(seed: number) {
    // 0だとxorshiftが機能しないので下駄を履かせる
    this.state = (seed >>> 0) || 0x9e3779b9;
  }
  // [0, bound) の整数を返す。Java の Random#nextInt(bound) / Go の rand.Intn(bound) 相当。
  nextInt(bound: number): number {
    this.state ^= this.state << 13;
    this.state ^= this.state >>> 17;
    this.state ^= this.state << 5;
    this.state >>>= 0;
    return this.state % bound;
  }
}

/** Memento: 狭いインタフェース。main.ts(Caretaker役)にはこの型しか見せない。 */
export interface Memento {
  readonly money: number;
}

/**
 * MementoImpl: 広いインタフェースを持つ実装本体。
 * exportしないので、このファイルの外(main.ts)からは型として参照できない。
 */
class MementoImpl implements Memento {
  readonly money: number;
  private fruits: string[] = [];

  constructor(money: number) {
    this.money = money;
  }

  addFruit(fruit: string): void {
    this.fruits.push(fruit);
  }

  getFruits(): string[] {
    return [...this.fruits];
  }
}

const FRUITS_NAME = ["リンゴ", "ぶどう", "バナナ", "みかん"]; // フルーツ名の表

/** Gamer: ゲームの主人公(Originator)。 */
export class Gamer {
  private _money: number;
  private fruits: string[] = [];
  private random: SeededRandom;

  constructor(money: number, seed: number) {
    this._money = money;
    this.random = new SeededRandom(seed);
  }

  get money(): number {
    return this._money;
  }

  // 賭ける…ゲームの進行
  bet(): void {
    const dice = this.random.nextInt(6) + 1; // サイコロを振る
    if (dice === 1) {
      // 1の目…所持金が増える
      this._money += 100;
      console.log("所持金が増えました。");
    } else if (dice === 2) {
      // 2の目…所持金が半分になる
      this._money = Math.floor(this._money / 2);
      console.log("所持金が半分になりました。");
    } else if (dice === 6) {
      // 6の目…フルーツをもらう
      const f = this.getFruit();
      console.log(`フルーツ(${f})をもらいました。`);
      this.fruits.push(f);
    } else {
      // それ以外…何も起きない
      console.log("何も起こりませんでした。");
    }
  }

  /**
   * スナップショットをとる。
   * 同じファイル内なので MementoImpl(広いインタフェース)を直接使える。
   * main.tsに公開するのは戻り値の型(Memento、狭いインタフェース)だけ。
   */
  createMemento(): Memento {
    const m = new MementoImpl(this._money);
    for (const f of this.fruits) {
      if (f.startsWith("おいしい")) {
        // フルーツはおいしいものだけ保存
        m.addFruit(f);
      }
    }
    return m;
  }

  /**
   * アンドゥを行う。
   * 受け取るのは狭いインタフェース(Memento型)だが、同ファイル内なので MementoImpl に
   * キャストして広いインタフェースを使う(main.tsからは型としては参照できないダウンキャスト)。
   * TypeScriptは構造的型付けなので `{ money: 0 }` のようなplainオブジェクトも
   * 型チェック上はMementoを満たしてしまう。`as`だけでは実行時チェックにならず
   * `getFruits()`が存在せず未定義参照で落ちるため、`instanceof`で実体を確認してから使う。
   */
  restoreMemento(memento: Memento): void {
    if (!(memento instanceof MementoImpl)) {
      throw new Error("restoreMemento: このMementoはGamerが発行したものではありません");
    }
    this._money = memento.money;
    this.fruits = memento.getFruits();
  }

  // 文字列表現。Java版のtoString()相当。
  toString(): string {
    return `[money = ${this._money}, fruits = [${this.fruits.join(", ")}]]`;
  }

  // フルーツを1個得る。
  private getFruit(): string {
    const prefix = this.random.nextInt(2) === 1 ? "おいしい" : ""; // JavaのnextBoolean()相当
    return prefix + FRUITS_NAME[this.random.nextInt(FRUITS_NAME.length)];
  }
}
main.ts
// Memento パターン: ゲームの主人公(Gamer)の所持金/フルーツをスナップショットする (Java版と同じ題材)
//
// 実行:
//   npx tsx main.ts            # デフォルトseed(314)
//   npx tsx main.ts 314        # seed指定
//
// Java版は演出用に毎ループ1秒(Thread.sleep)待って100ゲーム進めるが、パターンの本質と
// 無関係な人間向けの間(ま)なので省略し、即座に流す。
//
// 見どころ: main(Caretaker役)がimportできるのは狭いインタフェース(Memento型)だけ。
// 広いインタフェース(MementoImpl)はgame.tsの外にexportされていないので、
// 例えば `(memento as MementoImpl)` と書こうとしても型名を解決できずコンパイルエラーになる。
// 詳しくは game.ts の冒頭コメントを参照。

import { Gamer } from "./game";

function main(): void {
  const args = process.argv.slice(2);
  let seed = 314;
  if (args.length === 1) {
    seed = Number.parseInt(args[0], 10);
    if (Number.isNaN(seed)) {
      console.log("Usage: npx tsx main.ts [seed]");
      process.exit(1);
    }
  }

  const gamer = new Gamer(100, seed); // 最初の所持金は100
  let memento = gamer.createMemento(); // 最初の状態を保存しておく

  for (let i = 0; i < 100; i++) {
    console.log(`==== ${i}`); // 回数表示
    console.log(`現状:${gamer.toString()}`); // 現在の主人公の状態表示

    gamer.bet(); // ゲームを進める

    console.log(`所持金は${gamer.money}円になりました。`);

    // Mementoの取り扱いの決定
    if (gamer.money > memento.money) {
      console.log("    (だいぶ増えたので、現在の状態を保存しておこう)");
      memento = gamer.createMemento();
    } else if (gamer.money < Math.floor(memento.money / 2)) {
      console.log("    (だいぶ減ったので、以前の状態に復帰しよう)");
      gamer.restoreMemento(memento);
    }

    console.log("");
  }
}

main();
Python

Pythonには真のアクセス制御が無い。先頭アンダースコア(_add_fruit等)は「本来はGamerだけが触る想定」という命名規約でしかなく、コンパイラやインタプリタが強制するものではないため、main.py側からmemento._add_fruit(...)と書いても実行時には普通に成功してしまう。Go(package境界をコンパイラが強制)やTypeScript(exportしないシンボルは他ファイルからimportできない)と対比すると、Pythonの「狭い/広い」は完全に自己申告制であることがわかる。乱数はGoと同じく標準のrandom.Random(seed)を使い、シード指定で挙動を再現できる。

実行: python3 main.py [seed]

$ python3 GoF/patterns/Memento/python/main.py 314
memento.py
"""Memento (Java版 game.Memento に対応)。実行方法は main.py を参照。

Java版は「狭いインタフェース」(money取得のみ、public)と「広いインタフェース」
(コンストラクタ・add_fruit・get_fruits、package-private=gameパッケージ内、つまり
Gamerからしかアクセスできない)を使い分けている。

Pythonには真のアクセス制御が無い。先頭アンダースコア(_)は「本来はGamerだけが
触る想定」という命名規約でしかなく、コンパイラやインタプリタが強制するものではない。
Go(package境界をコンパイラが強制する)やTypeScript(exportしないシンボルは他ファイルから
importできない)と違い、Pythonではmain.py側から `memento._add_fruit("x")` と書いても
実行時には普通に成功してしまう("we're all consenting adults here"というPythonの設計思想)。
"""

from __future__ import annotations


class Memento:
    def __init__(self, money: int) -> None:
        self._money = money
        self._fruits: list[str] = []

    @property
    def money(self) -> int:
        """所持金を得る。狭いインタフェース: Caretaker(main.py)からも呼んでよい。"""
        return self._money

    def _add_fruit(self, fruit: str) -> None:
        """フルーツを追加する。広いインタフェース: 本来はGamerだけが呼ぶ想定。"""
        self._fruits.append(fruit)

    def _get_fruits(self) -> list[str]:
        """フルーツのコピーを得る。広いインタフェース。"""
        return list(self._fruits)
gamer.py
"""Gamer (Originator)。Java版 game.Gamer に対応。実行方法は main.py を参照。"""

from __future__ import annotations

import random

from memento import Memento

_FRUITS_NAME = ("リンゴ", "ぶどう", "バナナ", "みかん")  # フルーツ名の表


class Gamer:
    def __init__(self, money: int, seed: int) -> None:
        self._money = money
        self._fruits: list[str] = []
        self._random = random.Random(seed)  # 乱数発生器。seedから作るので挙動を再現できる

    @property
    def money(self) -> int:
        """現在の所持金を得る。"""
        return self._money

    def bet(self) -> None:
        """賭ける…ゲームの進行。"""
        dice = self._random.randrange(6) + 1  # サイコロを振る
        if dice == 1:  # 1の目…所持金が増える
            self._money += 100
            print("所持金が増えました。")
        elif dice == 2:  # 2の目…所持金が半分になる
            self._money //= 2
            print("所持金が半分になりました。")
        elif dice == 6:  # 6の目…フルーツをもらう
            fruit = self._get_fruit()
            print(f"フルーツ({fruit})をもらいました。")
            self._fruits.append(fruit)
        else:  # それ以外…何も起きない
            print("何も起こりませんでした。")

    def create_memento(self) -> Memento:
        """スナップショットをとる。

        同じ意図なら本来Goのようにコンパイラで強制したいところだが、Pythonなので
        _add_fruit という命名でしか「広いインタフェース」を表現できない(memento.py参照)。
        """
        m = Memento(self._money)
        for fruit in self._fruits:
            if fruit.startswith("おいしい"):  # フルーツはおいしいものだけ保存
                m._add_fruit(fruit)
        return m

    def restore_memento(self, memento: Memento) -> None:
        """アンドゥを行う。"""
        self._money = memento.money
        self._fruits = memento._get_fruits()

    def __str__(self) -> str:
        return f"[money = {self._money}, fruits = [{', '.join(self._fruits)}]]"

    def _get_fruit(self) -> str:
        """フルーツを1個得る。"""
        prefix = "おいしい" if self._random.randrange(2) == 1 else ""  # JavaのnextBoolean()相当
        return prefix + _FRUITS_NAME[self._random.randrange(len(_FRUITS_NAME))]
main.py
"""Memento パターン: ゲームの主人公(Gamer)の所持金/フルーツをスナップショットする (Java版と同じ題材)

実行:
    python3 main.py            # デフォルトseed(314)
    python3 main.py 314        # seed指定

Java版は演出用に毎ループ1秒(Thread.sleep)待って100ゲーム進めるが、パターンの本質と
無関係な人間向けの間なので省略し、即座に流す。

見どころ: main(Caretaker役)が触るのは gamer.money / memento.money という
狭いインタフェースだけ。memento._add_fruit のような広いインタフェースは
呼ばない(呼べてしまうが、呼ばない)。詳しくは memento.py の冒頭コメントを参照。
"""

from __future__ import annotations

import sys

from gamer import Gamer


def main() -> None:
    seed = 314
    if len(sys.argv) == 2:
        try:
            seed = int(sys.argv[1])
        except ValueError:
            print("Usage: python3 main.py [seed]")
            sys.exit(1)

    gamer = Gamer(100, seed)  # 最初の所持金は100
    memento = gamer.create_memento()  # 最初の状態を保存しておく

    for i in range(100):
        print(f"==== {i}")  # 回数表示
        print(f"現状:{gamer}")  # 現在の主人公の状態表示

        gamer.bet()  # ゲームを進める

        print(f"所持金は{gamer.money}円になりました。")

        # Mementoの取り扱いの決定
        if gamer.money > memento.money:
            print("    (だいぶ増えたので、現在の状態を保存しておこう)")
            memento = gamer.create_memento()
        elif gamer.money < memento.money // 2:
            print("    (だいぶ減ったので、以前の状態に復帰しよう)")
            gamer.restore_memento(memento)

        print()


if __name__ == "__main__":
    main()