デザインパターン mementoについて勉強した

mementoパターンとは

mementoとは思い出という意味です。
写真のようなものでその時の状態を記録しておくと、写真を見た時にその時のことを思い出すことができます。
プログラムではテキストエディタなどのアンドゥ機能もmementoパターンで考えられます。

コード例

ここではゲームのセーブ機能でmementoパターンを実装してみます。
プレイヤーはHEROを操作し、VILLAINを倒すことが目的です。
HEROは自分のHPなどの状態を保存しておき、ピンチになったら保存した状態まで時を戻して復活することができます。

HeroStatusクラス

package hero;

public class HeroStatus implements Cloneable{

	int hp ;

	HeroStatus(int hp){
		this.hp = hp;
	}

	public HeroStatus clone() {
		try {
			return (HeroStatus)super.clone();
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}
		return null;
	}
}

HeroStatusクラスはheroパッケージに配置しています。
これはhpフィールドやコンストラクタが同じパッケージ内のクラス、後述するHeroクラスからしかアクセスできないようにするためです。

hpフィールドはHEROの体力を表すパラメータです。
アクセス修飾子がないので、パッケージ内からのアクセスのみ許可されます。

コンストラクタの引数はhpです。
インスタンス化の際にhpを指定します。
これもアクセス修飾子がないので、パッケージ内からのみアクセス可です。

Cloneableインタフェースの実装やcloneメソッドのオーバライドがされており、このクラスはインスタンスの複製が可能になっています。

Heroクラス

package hero;

import java.util.Random;

public class Hero {

	private HeroStatus status ;
	private int mp ;

	public Hero(int hp,int mp) {
		status = new HeroStatus(hp);
		this.mp = mp;
	}

	public HeroStatus save() {
		if(mp<=0) {
			System.out.println("MPが足りない!");
			return null;
		}
		System.out.println("状態を保存した!!");
		mp--;
		return status.clone();
	}

	public void load(HeroStatus status) {
		if(status!=null) {
			if(mp<=0) {
				System.out.println("MPが足りない!");
				return;
			}
			System.out.println("HEROの時が戻った");
			mp--;
			this.status = status.clone();

		}else {
			System.out.println("しかし何も起きなかった");
		}
	}

	public int getHP() {
		return status.hp;
	}

	public int getMP() {
		return mp;
	}

	public int attack() {
		Random random = new Random();
		return random.nextInt(10);
	}

	public void damage(int damage) {
		System.out.println(String.format("HEROは%dダメージを受けた",damage));
		status.hp -= damage;
	}
}

ゲームユーザが操作するキャラクターです。
heroパッケージに入っています。

HeroStatusはフィールドでhpを持っていますが、mpフィールドはHeroクラスが持っています。
これにより、以前の状態に戻すことができるのはHeroStatusに含まれているHPだけになります。

コンストラクタではhpとmpの値を指定します。

saveメソッドがmementパターンの「記録」の部分です。
return status.clone();の行によって現在のstatusの状態を複製しています。

loadメソッドがmementパターンの「復元」の部分です。
this.status = status.clone();の行によって、渡されたインスタンスの複製を自身のフィールドに取り込んでいます。

getHPメソッド、getMPメソッドはhp,mpのゲッターです。

attackメソッドでランダムな攻撃力を返しています。

damageメソッドは渡された値を相手の攻撃力として自身のhpから減算します。

VillainStatusクラス

package villain;

public class VillainStatus{

	int hp ;

	VillainStatus(int hp) {
		this.hp = hp;
	}
}

VILLAINのステータスを保持するクラスです。
やっていることはほぼHeroStatusクラスと同じです。

Villainクラス

package villain;

import java.util.Random;

public class Villain {

	private VillainStatus status ;

	public Villain(int hp) {
		status = new VillainStatus(hp);
	}

	public int getHP() {
		return status.hp;
	}

	public int attack() {
		Random random = new Random();
		return random.nextInt(10);
	}

	public void damage(int damage) {
		System.out.println(String.format("VILLAINは%dダメージを受けた",damage));
		status.hp -= damage;
	}
}

こちらもHeroクラスとほぼ同じことをしています。
HeroクラスとVillainクラスの親となる共通のクラスを作って継承した方がシンプルになったかもしれません。
今回は勢いで作ってしまったのでこのままいきます。

実行

import java.util.Scanner;

import hero.Hero;
import hero.HeroStatus;
import villain.Villain;

public class Main {

	private Hero hero = new Hero(100,10);
	private Villain villain = new Villain(500);
	private HeroStatus save_data ;

	public Main() {
		Scanner scanner = new Scanner(System.in);
		while(hero.getHP()>0 && villain.getHP()>0) {
			System.out.println(String.format("HERO    HP:%d  MP:%d",hero.getHP(),hero.getMP()));
			System.out.println(String.format("VILLAIN HP:%d",villain.getHP()));
			System.out.println();
			System.out.println(String.format("%-8s %-8s %-8s","a:攻撃","s:セーブ","l:ロード"));
			System.out.print("行動を選択:");

			switch(scanner.nextLine()){
			case "a":
				System.out.println("HEROの攻撃!");
				villain.damage(hero.attack());
				break;
			case "s":
				System.out.println("HEROは状態を保存しようとした");
				save_data = hero.getMP()>0 ? hero.save() : save_data;
				break;
			case "l":
				System.out.println("HEROは状態を戻そうとした");
				hero.load(save_data);
				save_data = null;
				break;
			default:
				continue;
			}
			if(villain.getHP()>0) {
				System.out.println("VILLAINの攻撃!");
				hero.damage(villain.attack());
			}

			System.out.println("----------------");
		}

		if(hero.getHP()>0) {
			System.out.println("you win!");
		}else {
			System.out.println("you lose...");
		}

		scanner.close();
	}

	public static void main(String[] args) {
		new Main();
	}
}

いよいよゲームとして仕上げていきます。

heroフィールドはHERO、villainフィールドはVILLAINを操作するインスタンスです。
save_dataはheroが保存した状態を保持するフィールドです。初期状態では何も保存されていないのでnullが入っています。

whileループの条件は、どちらもHPが0より大きい場合、つまりどちらも倒れていない場合となります。

プログラムが少し長いので、mementoパターンに関係のある箇所を説明していきます。
switch内のcase "s"とcase "l"がその場所です。
case "s"ではheroのmpが0より多ければsaveメソッドを呼ぶようにしています。
これがmementoパターンの「記録」です。
記録した状態はsave_dataフィールドに保管されます。

case "l"では記録とは逆にsave_dataフィールドをheroのloadメソッドに渡しています。
これがmementoパターンの「復元」です。

特徴

例のようにゲームデータの保存のような場面で登場するパターンです。
そのほかにリドゥ、アンドゥであったり、テキストエディタでテキストを保存したり開いたりするときにも使えるかもしれません。

補足

HeroStatusクラスのhpフィールドは修飾子を付けませんでした。
修飾子をつけない場合、フィールドやメソッド、コンストラクタは同じパッケージ内からのみアクセスを許可します。
今回の例ではなぜ修飾子をつけなかったのでしょうか。

HeroStatusのインスタンスはHeroクラスのsaveメソッドから取得し、loadメソッドでHeroクラスの内部に戻されます。
ということは一度Heroクラスの外に出て行くわけです。
Heroクラスの外に出されたHeroStatusのインスタンスは誰がどうやって使おうとするかわかりません。
もしかしたら外部でhpに-100を入れようとするかもしれませんし、Heroが作っていないようなhp=999のHeroStatusインスタンスを勝手に作ろうとするかもしれません。
しかしHeroStatusのコンストラクタにもhpフィールドにも修飾子はついていないので、パッケージ外部のクラスではせいぜい変数を作ってそれに既存のHeroStatusインスタンスを入れておくくらいのことしかできません。
ですのでHeroやHeroStatusのようなheroパッケージ内のクラスは、HeroStatusに外部から不正な値が入れられることを心配する必要がなくなります。