mohuneko’s blog

かんばる駆け出しエンジニアのブログです

【Java】Memento パターン【デザインパターン】

駆け出しエンジニアがデザインパターンをもくもく勉強します

 こんな本で勉強しています🌟

目次

Mementoパターンについて

Mementoパターンとは

サンプルプログラム

  • Mementoパターンを使って、"サイコロの目に応じて、お金やフルーツ獲得する" 例を取り上げます
  • お金が貯まった時点で、Mementoクラスのインスタンスを作って、現在の状態(お金とフルーツ)を保存します
  • お金が減ってきたら、終了しないように、保存していたインスタンスを使って、以前の状態を復元します
  • 各クラスの役割は以下のようになっています
名前 役割
Gamer(Originator) Gameを行う主人公のクラス。Mementoのインスタンスを作る
Memento Gemerの状態を表すクラス
Main(Caretaker) Gameを進行させるクラス。Mementoのインスタンスを保存し、必要に応じてGamerの状態を復元する

f:id:mohuNeko:20210102003531p:plain

Mementoクラス

  • 主人公であるGemerの状態を表すクラスです。
  • money、fruitsは同じパッケージのGamerクラスから自由にアクセスできるようにするために、privateにしません
  • Mementoのコンストラクタにはpublicがついておらず(package private)、同じパッケージのクラス(Gamer)からしか使うことができません
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();
    }
}

Gamerクラス

  • Gameを行う主人公のクラスです。Mementoのインスタンスを作成します。
  • betメソッドで、もし主人公が破産していなかったら、サイコロをふって、その目で所持金やフルーツの個を変化させます
  • createMementoメソッドで、現在の情報のスナップショットをとります
    • 現在の状態をもとにMementoインスタンスを一個つくり、現在のGamerのインスタンスの状態を表現します
    • フルーツは美味しいものだけを保存しています
  • restoreMementoメソッドはUndoを行います
    • 与えられたMementoインスタンスを元に、自分の状態を復元します(ケアルガ! )
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クラスで動作確認

  • ゲームを進行させ、Mementoのインスタンスを保存しておき、必要に応じてGamerの状態を復元します。
  • mementoに、ある時点でのGamerの状態が保存されています
  • 所持金が減ったら、restoreMementoにmementoを与えて、所持金を元に戻します
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("");
        }
    }
}
  • 実行結果
==== 0
現状:[money = 100, fruits = []]
何も起こりませんでした。
所持金は100円になりました。

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

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

//中略

==== 10
現状:[money = 150, fruits = [バナナ, みかん]]
所持金が半分になりました。
所持金は75円になりました。
    (だいぶ減ったので、以前の状態に復帰しよう)

//中略

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

==== 99
現状:[money = 650, fruits = [おいしいみかん, おいしいリンゴ, おいしいぶどう, バナナ, おいしいぶどう, おいしいバナナ, おいしいぶどう, おいしいリンゴ, おいしいリンゴ]]
所持金が増えました。
所持金は750円になりました。

wide interfaceとnarrow interface

  • Mementoが、二種類のインターフェースを使い分けることで、オブジェクトのカプセル化の崩壊を防ぎます
  • wide interfaceは、"オブジェクトの状態を元に戻すための必要な情報が全て得られるメソッドの集合" です
    • Mementoの内部状態を全部公開してしまうので、使えるのはOriginator(Gamerクラス)のみです
    • OriginatorとMementoは非常に密接な関係にあります
  • narrow interfaceは、外部のCaretaker(Mainクラス)に公開するAPIです
    • 狭い、というのは、"内部状態の操作可能範囲が狭い" という意味です
    • 外部に公開する分、できることに制限をつけます
    • Caretakerは、Mementoの内部情報にアクセスできないので、作ってもらったMementoをブラックボックスとして、丸ごと保存します
    • Mementoのコンストラクタにもアクセスできないので、Mainの中でMementoのインスタンスを生成できません
      • Mementoの状態保存の際には、Gamerクラスを介して新しいインスタンスを生成します

Mementoパターンのメリット

  • Mementoパターンを使うと、undo、redohistory(作業履歴の作成)、snapshot(現在状態の保存) を行うことができます。
  • Caretaker(Mainクラス)とOriginator(Gamerクラス)では、以下のように役割分担を行っています
    • Caretaker では、"どのタイミングでsnapshot を撮るか"、"いつUndoするか"を決め、Mementoを保持します
    • 一方、Originator では、Mementoを作り与えられたMementoを使って自分の状態を戻します
  • 役割分担をしておけば、
    • 複数ステップのUndoを行うように変更したい
    • Undoだけでなく、現在の状態をファイルに保存したい
  • という修正を行いたいときにも、Originator を変更する必要はなくなります。

今日のポイント

  • Mementoパターンは、インスタンスの状態を表す役割を導入して、二種類のインターフェースを使い分けることで、カプセル化の破壊に陥ることなく保存/復元を行うパターンです
  • Mementoパターンを使うと、undo、redohistory(作業履歴の作成)、snapshot(現在状態の保存) を行うことができます。
  • Caretaker(Mainクラス)とOriginator(Gamerクラス)では、Mementoのインターフェースを利用して、役割分担を行うことで、高凝集かつ低結合な設計をすることができます

 本日もお疲れ様です😊