16. メメントパターン (Memento)
オブジェクトの中身を見せずにその瞬間の状態を保存しておき、あとで元に戻すメメントパターンについて学びましょう。
ひとことで言うと
オブジェクトが自分の状態を写真を撮るように自分で残しておき、あとでその写真を使って自分で元に戻れるようにするパターンです。写真を預かる側は、写真の中身がどうなっているかを知らなくて大丈夫です。
問題: セーブするには他人の中身を全部取り出さなければならないコード
冒険ゲームを作るとしましょう。勇者には体力、場所、かばんがあります。ゲームにはセーブがあって、勇者が倒れたら最後にセーブした場所からやり直します。
まず思いつくのは、セーブする関数が勇者の中身を1つずつ取り出して辞書に入れておき、ロードする時にまた1つずつ戻し入れる方法です。
【勇者】を設計しよう:
『体力』を【数字】の100にしよう
『場所』を【文字列】の「村」にしよう
『かばん』を【「木の剣」】にしよう
〈行く〉を作ろう(【文字列】の『行き先』):
『私』の『場所』を『行き先』にしよう
〈拾う〉を作ろう(【文字列】の『もの』):
『私』の『かばん』に『もの』を追加しよう
〈ダメージを受ける〉を作ろう(【数字】の『ダメージ』):
『私』の『体力』を(『私』の『体力』 - 『ダメージ』)にしよう
〈状態〉を作ろう():
枠「{『私』の『場所』} · 体力 {『私』の『体力』} · かばん {『私』の『かばん』}」を出力しよう
(参考:セーブする側が勇者の中身を1つずつ取り出して辞書に入れます。かばんはリストなので、そのまま入れると同じリストを一緒に指してしまいます。だから1つずつ移して入れてコピーします)
〈セーブする〉を作ろう(『勇者』):
『かばんのコピー』を【】にしよう
『勇者』の『かばん』の『もの』ごとに繰り返そう:
『かばんのコピー』に『もの』を追加しよう
{「体力」: 『勇者』の『体力』, 「場所」: 『勇者』の『場所』, 「かばん」: 『かばんのコピー』}を返そう
(参考:ロードする時も、中身を1つずつ戻し入れます。ここでもかばんをコピーしないと、セーブが壊れます)
〈ロードする〉を作ろう(『勇者』, 『セーブ』):
『勇者』の『体力』を(『セーブ』の「体力」)にしよう
『勇者』の『場所』を(『セーブ』の「場所」)にしよう
『かばんのコピー』を【】にしよう
(『セーブ』の「かばん」)の『もの』ごとに繰り返そう:
『かばんのコピー』に『もの』を追加しよう
『勇者』の『かばん』を『かばんのコピー』にしよう
『勇者』を新しい【勇者】()にしよう
『勇者』の〈行く〉(「森」)を実行しよう
『勇者』の〈拾う〉(「薬草」)を実行しよう
『セーブ』を〈セーブする〉(『勇者』)にしよう
「[セーブしました]」を出力しよう
『勇者』の〈状態〉()を実行しよう
『勇者』の〈行く〉(「洞窟」)を実行しよう
『勇者』の〈拾う〉(「たいまつ」)を実行しよう
『勇者』の〈ダメージを受ける〉(120)を実行しよう
『勇者』の〈状態〉()を実行しよう
「[倒れました。セーブした場所からやり直します]」を出力しよう
〈ロードする〉(『勇者』, 『セーブ』)を実行しよう
『勇者』の〈状態〉()を実行しよう
実行結果
[セーブしました]
森 · 体力 100 · かばん [木の剣, 薬草]
洞窟 · 体力 -20 · かばん [木の剣, 薬草, たいまつ]
[倒れました。セーブした場所からやり直します]
森 · 体力 100 · かばん [木の剣, 薬草]
ちゃんと動きます。でも、ゲームが大きくなると痛くなります。
- セーブする側が勇者の中身を全部知っています。
〈セーブする〉と〈ロードする〉は、勇者に『体力』、『場所』、『かばん』があることと、その名前まで正確に知らなければなりません。 - 勇者に何か1つ増えると、入れ忘れやすいです。
『お金』ができたら、〈セーブする〉と〈ロードする〉の両方を直さなければなりません。片方だけ直すと、エラーも出ずにお金だけがこっそり戻らなくなります。 - コピーしなければならないことまで、使う側が気をつけています。 かばんはリストなので、そのまま入れるとセーブと勇者が同じリストを一緒に指してしまいます。すると、洞窟で拾ったたいまつがセーブにも入ってしまいます。だから両方の関数に、1つずつ移して入れるコードを入れました。
- 勇者の中身を隠せません。 外から取り出さなければならないので、
『体力』に隠そうが使えません。どこからでも体力を書き換えられます。 - セーブもただの辞書です。 だれでも
『セーブ』の「体力」を書き換えておけます。
解決策: 勇者に自分の写真を自分で撮らせる
メメントパターンは、セーブを3つの役割に分けます。
- オリジナル(勇者)が、自分の状態を入れたメメント(セーブポイント)を自分で作ります。自分の中身は自分がいちばんよく知っているからです。
- メメントは、その瞬間の状態を入れておくだけです。入っている値を変える方法は渡しません。
- 管理人(セーブ置き場)は、メメントを積んでおいて、必要な時に渡すだけです。メメントの中をのぞいたり、変えたりしません。
- 元に戻す時は、オリジナルがメメントを受け取って、自分で自分の状態を戻します。
たとえ: インスタント写真
旅先でインスタント写真を撮ると、写真にはその瞬間の姿がそのまま残ります。アルバムは写真をはさんでおくだけで、写真の中の場面を変えたりしません。あとで「あの時のあの場所」に戻りたくなったら、写真を取り出して見て、そのとおりに立てばいいんです。写真はメメント、アルバムは管理人、写真を撮ってその場所に戻る人はオリジナルです。
だれが何をするの?
| 役割 | この例では | やること |
|---|---|---|
| オリジナル | 【勇者】 |
自分の状態でセーブポイントを作り、セーブポイントで自分から元に戻ります |
| メメント | 【セーブポイント】 |
その瞬間の状態を入れておき、取り出して見る方法だけを渡します |
| 管理人 | 【セーブ置き場】 |
セーブポイントを積んでおいて渡します。中身は知りません |
カナデで作る
同じゲームを、メメントパターンで作り直してみましょう。
(参考:ステップ1 — セーブポイント(メメント)。その瞬間の勇者をそのまま入れておき、変える方法は渡しません)
【セーブポイント】を設計しよう:
『体力』を【数字】の0に隠そう
『場所』を【文字列】の「」に隠そう
『かばん』を【】に隠そう
最初に作られる時(【数字】の『体力』, 【文字列】の『場所』, 『かばん』)次のようにしよう:
『私』の『体力』を『体力』にしよう
『私』の『場所』を『場所』にしよう
(参考:かばんは1つずつ移してコピーしておきます。そうすれば勇者のかばんが変わっても、セーブポイントはそのままです)
『コピー』を【】にしよう
『かばん』の『もの』ごとに繰り返そう:
『コピー』に『もの』を追加しよう
『私』の『かばん』を『コピー』にしよう
【数字】を返す〈体力〉を作ろう():
『私』の『体力』を返そう
【文字列】を返す〈場所〉を作ろう():
『私』の『場所』を返そう
(参考:かばんもコピーして渡します。受け取った側が変えても、セーブポイントはそのままです)
〈かばん〉を作ろう():
『渡すかばん』を【】にしよう
『私』の『かばん』の『もの』ごとに繰り返そう:
『渡すかばん』に『もの』を追加しよう
『渡すかばん』を返そう
(参考:ステップ2 — 勇者(オリジナル)。中身は隠し、セーブポイントを作ることと、セーブポイントに戻ることは自分でします)
【勇者】を設計しよう:
『体力』を【数字】の100に隠そう
『場所』を【文字列】の「村」に隠そう
『かばん』を【「木の剣」】に隠そう
〈行く〉を作ろう(【文字列】の『行き先』):
『私』の『場所』を『行き先』にしよう
〈拾う〉を作ろう(【文字列】の『もの』):
『私』の『かばん』に『もの』を追加しよう
〈ダメージを受ける〉を作ろう(【数字】の『ダメージ』):
『私』の『体力』を(『私』の『体力』 - 『ダメージ』)にしよう
〈状態〉を作ろう():
枠「{『私』の『場所』} · 体力 {『私』の『体力』} · かばん {『私』の『かばん』}」を出力しよう
【セーブポイント】を返す〈セーブする〉を作ろう():
新しい【セーブポイント】(『私』の『体力』, 『私』の『場所』, 『私』の『かばん』)を返そう
〈ロードする〉を作ろう(【セーブポイント】の『ポイント』):
『私』の『体力』を『ポイント』の〈体力〉()にしよう
『私』の『場所』を『ポイント』の〈場所〉()にしよう
『私』の『かばん』を『ポイント』の〈かばん〉()にしよう
(参考:ステップ3 — セーブ置き場(管理人)。セーブポイントを積んでおくだけで、中身はのぞきません)
【セーブ置き場】を設計しよう:
『ポイントたち』を【】に隠そう
〈残す〉を作ろう(【セーブポイント】の『ポイント』):
『私』の『ポイントたち』に『ポイント』を追加しよう
【セーブポイント】を返す〈最後〉を作ろう():
『個数』を『私』の『ポイントたち』の『長さ』にしよう
『私』の『ポイントたち』の『個数』番目を返そう
『勇者』を新しい【勇者】()にしよう
『置き場』を新しい【セーブ置き場】()にしよう
『勇者』の〈行く〉(「森」)を実行しよう
『勇者』の〈拾う〉(「薬草」)を実行しよう
『置き場』の〈残す〉(『勇者』の〈セーブする〉())を実行しよう
「[セーブしました]」を出力しよう
『勇者』の〈状態〉()を実行しよう
『勇者』の〈行く〉(「洞窟」)を実行しよう
『勇者』の〈拾う〉(「たいまつ」)を実行しよう
『勇者』の〈ダメージを受ける〉(120)を実行しよう
『勇者』の〈状態〉()を実行しよう
「[倒れました。セーブした場所からやり直します]」を出力しよう
『勇者』の〈ロードする〉(『置き場』の〈最後〉())を実行しよう
『勇者』の〈状態〉()を実行しよう
実行結果
[セーブしました]
森 · 体力 100 · かばん [木の剣, 薬草]
洞窟 · 体力 -20 · かばん [木の剣, 薬草, たいまつ]
[倒れました。セーブした場所からやり直します]
森 · 体力 100 · かばん [木の剣, 薬草]
結果はリファクタリング前と1行も変わりません。でも今は、勇者の中身はすべて隠れていて、セーブ置き場はセーブポイントの中に何があるのかまったく知りません。
ステップごとにもう一度
- ステップ1 — セーブポイント(メメント)。
【セーブポイント】は、体力、場所、かばんを隠したまま入れておきます。取り出して見る〈体力〉、〈場所〉、〈かばん〉はありますが、値を変える機能はありません。かばんは作る時に1回、渡す時に1回コピーするので、勇者があとで何を拾ってもセーブポイントはそのままです。 - ステップ2 — 勇者(オリジナル)。
〈セーブする〉は自分の状態でセーブポイントを作って返し、〈ロードする〉はセーブポイントを受け取って自分の状態を戻します。「何をセーブしなければならないか」を知っているのは勇者自身だけです。 - ステップ3 — セーブ置き場(管理人)。
【セーブ置き場】は、セーブポイントを〈残す〉で積んで、〈最後〉で渡すだけです。セーブポイントの中に何があるかを知らなくていいし、知る必要もありません。 - 使う側。
『置き場』の〈残す〉(『勇者』の〈セーブする〉())と『勇者』の〈ロードする〉(『置き場』の〈最後〉())の2行で、セーブもロードもできます。体力のことも、かばんのコピーのことも気にしません。
カナデには「勇者だけが見られる」がありません
本に出てくるメメントでは、オリジナルだけがメメントの中を見られるようにすることもあります。カナデの隠そうは「そのクラスの中だけ」なので、【勇者】が【セーブポイント】の隠した値を直接読むことはできません。そこでここでは、取り出して見る機能だけがあって、変える機能はないセーブポイントを作りました。セーブ置き場がその機能を呼ぶことはできても、セーブポイントを変える機能はどこにもありません。
セーブポイントの中身は隠れているので、外からこっそり物を入れようとすると、その場で止められます。
【セーブポイント】を設計しよう:
『かばん』を【】に隠そう
最初に作られる時(『かばん』)次のようにしよう:
『コピー』を【】にしよう
『かばん』の『もの』ごとに繰り返そう:
『コピー』に『もの』を追加しよう
『私』の『かばん』を『コピー』にしよう
『ポイント』を新しい【セーブポイント】(【「木の剣」】)にしよう
(参考:セーブポイントにこっそり物を入れようとすると止められます)
『ポイント』の『かばん』に「伝説の剣」を追加しよう
勇者に新しい状態を足してみると
勇者に『お金』ができたとしましょう。
- 前のやり方:
〈セーブする〉と〈ロードする〉に、お金を1つずつ足さなければなりません。1つでも忘れると、お金だけが戻りません。 - メメントパターン:
【勇者】と【セーブポイント】だけを直せば済みます。セーブ置き場と使う側は1文字も変わりません。セーブを扱うコードが、勇者とセーブポイントの2つの間にまとまっているからです。
やってみよう
上のコードの勇者に『お金』(最初は0)と〈稼ぐ〉(【数字】の『金額』)を足してみてください。セーブポイントにもお金を入れ、〈ロードする〉がお金も戻すようにすればいいんです。森で50を稼いでセーブしたあと、洞窟でさらに30を稼いで倒れたら、ロードしたあとのお金は50でなければなりません。セーブ置き場といちばん下の使うコードは、そのままで大丈夫です。
どんな時に使うといいの?
- 元に戻す、セーブとロード、チェックポイントが必要な時
- オブジェクトの状態を保存しなければならないけれど、その中身を外に見せたくない時
- 状態を作る値がいくつもあって、1つずつ保存していると忘れやすい時
- 試しに変えてみて、気に入らなければまるごと元どおりに戻したい時
いいところと気になるところ
| いいところ | 気になるところ |
|---|---|
| オリジナルの中身を隠したまま、保存して元に戻せます | 保存するたびに状態をまるごと入れるので、状態が大きいと記憶する場所をたくさん使います |
| 何を保存するかはオリジナルだけが知っているので、忘れることが減ります | リストのように一緒に指す値は、コピーを忘れないようにしなければなりません |
| 管理人はセーブポイントを積んでおくだけなので単純です | セーブポイントを積みすぎると、古いものをいつ捨てるか決める必要があります |
| セーブポイントには変える機能がないので、保存した瞬間のまま残ります | カナデではオリジナルだけが読めるようには止められないので、読む機能はだれでも呼べます |
早すぎるパターンは毒です。
保存する値が数字1つだけなら、その数字を変数に入れておけば十分です。状態を作る値がいくつもあって、その中身を隠したくなった時に、メメントを取り出しても遅くありません。
ほかのパターンとの関係
- コマンドパターンとは、元に戻すやり方が違います。 コマンドは「何をしたか」を覚えて逆にやって戻し、メメントは「その時どうだったか」をまるごと覚えてその状態に戻ります。逆にやるのが難しい仕事ならメメントが楽で、状態がとても大きいならコマンドのほうが軽いです。2つを一緒に使って、コマンドごとに実行前のセーブポイントを残しておくこともあります。
- イテレーターパターンでセーブ一覧を見せられます。 セーブ置き場が反復子を作ってあげれば、セーブ一覧の画面はセーブ置き場の中身を知らなくても、セーブポイントを1つずつ取り出して見せられます。
- ステートパターンとは名前が似ているだけです。 ステートパターンは状態によってふるまいを変えるパターンで、メメントは状態を保存して元に戻すパターンです。