Featured image of post コードウィザード【Command】怖くて、もう石ひとつ動かせない〜一手ずつを呪符に変え、いつでも巻き戻せるようにする〜

コードウィザード【Command】怖くて、もう石ひとつ動かせない〜一手ずつを呪符に変え、いつでも巻き戻せるようにする〜

庭師リンは、意匠板の操作が直接データを書き換えるだけで取り消せず、大胆な配置換えを試せずにいた。見習い魔術師ルークは操作の記録を一つの型にまとめようとして歪みに気づき、Kotlinで操作をCommandオブジェクトに実体化し、undo/redoを実現する。

動かした石は、二度と戻らない

うちはリン。庭師をやっている。石を組み、灯籠を据え、植木を配し、依頼された庭の姿を仕上げるのが仕事だ。

いきなり本物の庭に手を入れるわけじゃない。まずは「意匠板」——石や灯籠や植木を模した符を、盤の上で動かしたり消したりして、実際に据える前に配置を試すための道具を使う。頭の中の思いつきを、盤の上で先に確かめる。それが、うちの仕事のやり方だった。

ところがこの意匠板、動かす・消すの操作が、盤のデータをその場で直に書き換えるだけの作りになっている。一度動かした石も、消した灯籠も、二度と元の位置・元の姿には戻らない。取り消しも、やり直しも、履歴も、何も無い。

頭の中では「もっとこう配置したら映えるのに」と分かっていても、うちはいつからか、大胆な配置換えを試すのをやめてしまっていた。戻せない恐れが、先に立つ。腕は落ちてないはずなのに、うちの手がけた庭は、いつからか似たり寄ったりになっていた。

先日、決定打があった。ある由緒ある庭で、代々受け継がれてきた灯籠を一つ、意匠板の上で「試しに」動かしてみたことがある。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 庭の意匠板に置く一つひとつの符(石・灯籠・植木)。位置は val(一度置いたら書き換えない)
data class Shape(val id: String, val kind: String, val x: Int, val y: Int)

// 意匠板:符の配置を直に持つ。動かす・消す操作が、盤のデータをその場で書き換えるだけ
class GardenBoard {
    private val shapes = mutableMapOf<String, Shape>()

    fun place(shape: Shape) {
        shapes[shape.id] = shape
    }

    fun move(id: String, dx: Int, dy: Int) {
        val current = shapes[id] ?: return
        shapes[id] = current.copy(x = current.x + dx, y = current.y + dy)   // 上書きしたら、元の位置はもう覚えていない
    }

    fun remove(id: String) {
        shapes.remove(id)   // 消したら、消した符の姿はどこにも残らない
    }

    fun shapeAt(id: String): Shape? = shapes[id]
}

data class は、値をひとまとまりに運ぶための器だ。copy() は、一部だけ変えた新しい姿を作る書き方——今回なら、xyだけを変えた新しいShapeを作って、そのまま盤に置き直している。

問題は、その置き直し方にある。moveremoveも、盤のデータをその場で直接書き換えるだけで、「動かす前」「消す前」の姿をどこにも控えていない。一度上書きしたら、元の値はもう覚えていない——それだけのことだ。

あの灯籠を試しに動かした後、うちは思ったより見栄えが悪く、元の位置に戻そうとした。だが盤には、元の位置を覚えている仕組みが無い。うちは自分の記憶と控えのスケッチだけを頼りに、位置を勘で逆算して打ち返した。

1
2
3
4
5
6
7
8
val original = Shape("lantern-01", "灯籠", 10, 5)
board.place(original)

board.move("lantern-01", 3, 0)     // 試しに動かす → (13, 5)

// 元に戻したいが、盤のどこにも「動かす前は (10, 5) だった」という記録は無い。
// うちは自分の記憶とスケッチを頼りに、逆算して打ち返すしかない
board.move("lantern-01", -3, -1)   // 記憶違いで、縦のずれを勘違いしたまま打ち返してしまう

盤は「間違っている」とは一切教えてくれない。淡々と、間違った位置を返すだけだ。据えてから、依頼主に「前より心持ち右に寄っている気がする」と指摘され、うちは冷や汗をかいた。幸い大事には至らなかったが、二度とやりたくない。

名より先に、仕組みの評判

花市場に集う庭師仲間うちで、ここのところ、ある噂を耳にしていた。

「丘のアカデミーの見習いが、何かを直したらしい」。ただし、その見習いの名前より先に、直した中身のほうが先に伝わっていた。「切れ味のいい魔法を使えるようになった、って話じゃない。“やり直しがきく"仕組みを作ったらしい」。

うちは、その言い回しに引っかかった。名前より先に、成果の中身が伝わってくる。よほどのことなのか、あるいはただの尾ひれか。どちらにせよ、一度見ておく価値はありそうだった。うちは道具を仕舞い、アカデミーの丘を上った。

工房は、本棚と、宙にふわふわ浮かぶ光の粒でいっぱいだった。うちは物珍しさよりも、まず部屋の隅にちらりと目をやった。白髪の年寄りが一人、椅子に座っている。庭仕事とは縁のなさそうな、隠居の爺さんだろう——それだけ思って、うちはすぐに目を逸らした。

肝心の見習いは、机のほうにいた。一枚の護符を、几帳面に一つ折っては、指先でそっと元の折り目まで開き戻し、寸分違わず同じ形に戻るかを確かめてから、また折り直す、ということを飽きずに繰り返している。

「……戻せる形のまま、進めているか」

そう独りごちながら。うちが声をかけると、護符を置いて顔を上げた。

「あ……意匠板の、ご相談ですか」

これが噂の見習いか。派手なところは、どこにも無い。

うちは、意匠板の写しと、灯籠のずれの一件を語った。ルーク——と名乗ったその見習いは、写しをじっと見て、静かに言った。

「これは……一度上書きしたら、元の値はもう、どこにも残っていませんね。動かす前、消す前の姿を、盤がまるで覚えていない」

「そうなんだよ」とうちは頷いた。「だから、うちは怖くて、大胆には動かせねえ」

ルークが名付けたところによると、これは取り消し・履歴のできない手続き的操作——操作が盤のデータを直接書き換えるだけで、元へ戻る道がどこにも用意されていない作り、なんだそうだ。

「一度動かしたら、もう後戻りはできねえんだ」。うちの言葉を、ルークは黙って受け止めていた。

一つに、まとめておけば

ルークは写しをしばらく見つめてから、うちに向かってでなく、低く独りごちた。

「この手順……逐一控えておけば、あとから読み解いて、戻す分岐を書けば済む」

宣言というより、静かな見積もりだった。うちは机の端から、それをちらりと見ているだけだった。

ルークは、操作の記録を一つの型にまとめ始めた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// ルーク(独り言)「この手順……逐一控えておけば、あとから読み解いて、戻す分岐を書けば済む」
data class ActionLog(
    val type: String,              // "MOVE" / "DELETE" / "ADD" ……操作の種類が増えるたび、この型に足していくことになる
    val id: String,
    val dx: Int? = null, val dy: Int? = null,       // MOVE でしか使わない
    val removedShape: Shape? = null,                 // DELETE でしか使わない
    // ADD 用の情報も、この先また別のフィールドとして足すことになる……
)

fun undo(log: ActionLog, board: GardenBoard) {
    when (log.type) {
        "MOVE" -> board.move(log.id, -(log.dx ?: 0), -(log.dy ?: 0))
        "DELETE" -> log.removedShape?.let { board.place(it) }
        // "ADD" -> まだ書けていない……
        else -> {}
    }
}

移動の巻き戻しは書けた。削除の巻き戻しも、消した符を覚えておけば書けた。だが、追加の巻き戻しを書こうとして、ルークの手が止まった。

「追加の巻き戻しは、消すだけでいいのに……この記録の型には、もう使わない場所が、いくつも空いている」

dxdyremovedShape——操作の種類によっては、nullのまま残る場所が目立ってくる。移動の記録には削除の情報が要らず、削除の記録には移動の情報が要らない。それでも同じ一つの型に収めようとするから、使わない場所が空いたままになる。

ルークの手が止まったのを見て、うちの中で、あの半日の冷や汗がふっと蘇った。

(……やっぱり、そう簡単にはいかないのか)

声には出さない。ただ、期待した分だけ、肩が強張るのを感じた。

隅の年寄りは、あいかわらずそこに座っていた。小さな砂時計を手に取り、無言でひっくり返しては、また戻す、ということを繰り返している。うちにはそれが、暇つぶしの手すさびに見えた。何をしているのかは、気にも留めなかった。

この折り目と、同じだ

ルークは、書きかけの記録から手を離した。机の端に置いていた、さっき折っていたあの護符を、ふと取り上げる。指先でゆっくり、元の折り目まで開き戻してみせながら、低く呟いた。

「……この折り目と、同じだ」

うちには、何のことか分からなかった。ルークは続けた。

「一つの記録に、全部の操作の"戻し方"を詰め込もうとしていた。でも、移動を戻すのに要る情報と、削除を戻すのに要る情報は、そもそも形が違う」

そこで、ルークは少し考えるように間を置いた。

「前に別のご依頼で、“巨大な分岐"を見たことがあります。あのときは、あり得る状態の組み合わせを、一つ残らず先回りして潰そうとして、失敗しました。でも、今度のこれは違う。組み合わせを網羅しようとしているんじゃない。一つの入れ物に、形の違う戻し方を、無理に詰め込もうとして歪んでいるんです」

うちには魔術のことは分からないが、その口ぶりからは、似たような壁に二度目にぶつかっているんじゃない、ということだけは伝わってきた。

「操作ごとに、戻すための情報の形が違うなら……」ルークが呟く。「操作そのものを、それぞれ自分で戻せる一つの実体にすればいい。この護符みたいに、一つひとつが、自分の折り目を、自分で覚えていればいい」

誰にも急かされたわけじゃない。ルークは、自分で手を止めて、自分で気づいたようだった。

操作そのものを、一枚の呪符に

ルークは、新しい設計に取りかかった。

「一つひとつの操作を、実行も、取り消しもできる形にします」

1
2
3
4
5
// 操作そのものを、実行(execute)も取り消し(undo)もできる一つの実体にする
interface Command {
    fun execute()
    fun undo()
}

interface——「こう呼べばこう応える」という約束の形だ。今回は、execute(実行する)とundo(取り消す)という、二つの口を持つ約束にした。

 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
class MoveCommand(
    private val board: GardenBoard,
    private val id: String,
    private val dx: Int,
    private val dy: Int,
) : Command {
    override fun execute() = board.move(id, dx, dy)
    override fun undo() = board.move(id, -dx, -dy)   // 自分の増分だけを知っていればよい
}

class DeleteCommand(
    private val board: GardenBoard,
    private val id: String,
) : Command {
    private var removed: Shape? = null   // 消す前の姿を、自分でこっそり覚えておく

    override fun execute() {
        removed = board.remove(id)       // remove は「消した中身」をそのまま返してくれる(無ければ null)
    }
    override fun undo() {
        removed?.let { board.place(it) }
    }
}

class AddCommand(
    private val board: GardenBoard,
    private val shape: Shape,
) : Command {
    override fun execute() = board.place(shape)
    override fun undo() {
        board.remove(shape.id)   // 追加の逆は、ただ消すだけ
    }
}

「移動なら、動かした分だけ。削除なら、消した姿だけ。それぞれが、自分の戻し方に要る情報だけを持ちます。他の操作のことは、何も知らなくていい」

操作そのものを、実行も取り消しもできる一つの実体にする——これがCommand、操作をオブジェクトとして表現し、実行だけでなく取り消しも扱えるようにする定石だ、とルークは言った。

うちは、さっきの空いた場所だらけの記録を思い出した。「なるほどな……一つにまとめようとするから、余計な空きができる。それぞれが自分のことだけ覚えてりゃ、余計な場所は要らないってことか」

「はい」

DeleteCommandremoveについて、ルークが付け加えた。「盤のremoveも、このタイミングで書き換えました。消した中身を、そのまま返すようにしたんです」。

1
2
// GardenBoard.remove を書き換え:消した中身を、そのまま返すようにする
fun remove(id: String): Shape? = shapes.remove(id)   // 消した中身をそのまま返してくれる(無ければ null)

「前は、消したら何も返さない作りでした。消す前にわざわざ確認しにいかなくて済むように、MutableMapremoveが持っている、消した値をそのまま返してくれる——無ければnullだ、という性質を、そのまま活かす形にしました」

一つ、うちには気になることがあった。

「庭の"全体の写し"を、動かすたびごっそり取っておけば、よくないのか。一手ごとに、盤ぜんぶの姿を保存しておくとか」

ルークは首を振った。

「写しを毎回まるごと取ると、庭が大きくなるほど写しが重くなりますし……それに、“何をしたか"という記録そのものが消えてしまいます。操作そのものを覚えておけば、軽いですし、“何をしたか"も一緒に残ります」

なるほど、とうちは思った。全部を丸ごと保存しておくやり方と、やったことだけを覚えておくやり方は、似ているようで違う。

続けて、ルークは操作を積んでおく係を書いた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 使った操作を積み、戻す・やり直すを管理する係
class CommandHistory {
    private val undoStack = ArrayDeque<Command>()
    private val redoStack = ArrayDeque<Command>()

    fun execute(command: Command) {
        command.execute()
        undoStack.addLast(command)
        redoStack.clear()   // 新しい一手をしたら、やり直し履歴は捨てる
    }

    fun undo() {
        val command = undoStack.removeLastOrNull() ?: return   // 履歴が空なら、何もしない
        command.undo()
        redoStack.addLast(command)
    }

    fun redo() {
        val command = redoStack.removeLastOrNull() ?: return
        command.execute()
        undoStack.addLast(command)
    }
}

ArrayDequeは、後に積んだものから先に取り出せる、一列の入れ物だ。addLastが積む、removeLastOrNullが、直近に積んだものを取り出す——ただし、中身が空ならnullが返る。

「積んだものが無かったら、どうなる」とうちは訊いた。

removeLastOrNullは、積んだものが無ければnullを返します。?: return——nullだったら、そのときは何もせず帰る、という書き方です。“魔力が空かもしれない"を先に認めておく、というのと同じ考え方です」

「一度戻したものを、またやり直せるのか」

「はい。redo用にも別に積んでおきます。ただし、戻した後に新しい操作をしたら、やり直し履歴は捨てます。一度枝分かれした先の"もしも"を、また辿れてしまうと、庭の姿がどっちつかずになりますから」

ルークが呪文を書き終えると、机の上には何枚もの呪符と、それらを収める魔導書のような履歴管理の入れ物が整然と並んでいました。 うちは腕を組み、頭の中でそのつながりを反芻してみます。

「……なるほどな。全体の流れがようやく見えてきたぞ。うちが操作をするたびに、この履歴管理の魔導書が呪符を預かり、戻すときはその呪符自身に元に戻す術を実行させる。そしてすべての呪符は、うちの意匠板を裏で操作している……こういうことかい?」

「はい、その通りです」ルークは誇らしげに頷き、一枚の羊皮紙にその関係図を描き出してみせました。

Commandパターンの構造設計図。CommandHistoryがCommandの履歴を管理し、MoveCommand、DeleteCommand、AddCommandがCommandインターフェースを実装してGardenBoardを操作する関係を示すクラス図

「こうして見ると、実に綺麗だな」とうちはため息をつきました。「魔導書は呪符の共通の約束(インターフェース)しか知らないから、これから新しい操作が増えても、魔導書そのものを書き換える必要がないってわけだ」

紙の庭に、石を置いて

「本物の庭に石を置く前に」とルークは言った。「結界の中に紙の庭を敷いて、一手ずつ確かめます」

模擬詠唱——本番の前に、安全な形で試しに動かして確かめる作法だ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 1. MoveCommand の execute/undo: 動かした後にundoすると、元の座標に正確に戻る
val board1 = GardenBoard()
board1.place(Shape("lantern-01", "灯籠", 10, 5))

val moveCmd = MoveCommand(board1, "lantern-01", 3, 0)
moveCmd.execute()
assertEquals(Shape("lantern-01", "灯籠", 13, 5), board1.shapeAt("lantern-01"))

moveCmd.undo()
assertEquals(Shape("lantern-01", "灯籠", 10, 5), board1.shapeAt("lantern-01"))

assertEqualsは、動かした後の姿が、狙いどおりかを照らします。動かして(13, 5)、戻して、ちゃんと(10, 5)に正確に戻ることを確かめました」

続けて、削除の巻き戻し。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 2. DeleteCommand の execute/undo: 消した後にundoすると、消す前と寸分同じ Shape が復元される
val board2 = GardenBoard()
val originalStone = Shape("stone-01", "石", 2, 2)
board2.place(originalStone)

val deleteCmd = DeleteCommand(board2, "stone-01")
deleteCmd.execute()
assertEquals(null, board2.shapeAt("stone-01"))

deleteCmd.undo()
assertEquals(originalStone, board2.shapeAt("stone-01"))

「消す前の姿を、消す前とまったく同じ形で戻す。うちの、あの灯籠のときの悩みに、そのまま応えます」

うちは、あの日のことを思い出しながら、その一行を目で追った。勘で逆算する必要が、もう無い。

さらに、複数の操作を重ねてから戻す検め。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 3. 任意の操作列のundo/redo: 移動→削除→追加、の順に実行した後、
//    3回undoすると元の状態に戻り、3回redoすると再び同じ結果になる
val board3 = GardenBoard()
board3.place(Shape("tree-01", "植木", 1, 1))
board3.place(Shape("stone-02", "石", 5, 5))
val addedLantern = Shape("lantern-02", "灯籠", 20, 20)

val history = CommandHistory()
history.execute(MoveCommand(board3, "tree-01", 4, -1))
history.execute(DeleteCommand(board3, "stone-02"))
history.execute(AddCommand(board3, addedLantern))

// 3回undoで初期状態に戻る
history.undo(); history.undo(); history.undo()
assertEquals(Shape("tree-01", "植木", 1, 1), board3.shapeAt("tree-01"))
assertEquals(Shape("stone-02", "石", 5, 5), board3.shapeAt("stone-02"))
assertEquals(null, board3.shapeAt("lantern-02"))

// 3回redoで操作後の状態に戻る
history.redo(); history.redo(); history.redo()
assertEquals(Shape("tree-01", "植木", 5, 0), board3.shapeAt("tree-01"))
assertEquals(null, board3.shapeAt("stone-02"))
assertEquals(addedLantern, board3.shapeAt("lantern-02"))

「移動して、削除して、追加して。3回戻せば、最初の庭にきっちり戻ります。3回やり直せば、また同じ庭に」

異常系も、二つ検めた。

1
2
3
4
5
6
7
8
9
// 4. 履歴が空のときのundoが安全に何もしない(異常系): 例外を投げず、盤の状態が変わらない
val board4 = GardenBoard()
val untouched = Shape("rock-01", "石", 0, 0)
board4.place(untouched)

val emptyHistory = CommandHistory()
emptyHistory.undo()   // 何も積んでいない状態でundoを呼んでも、何もしない

assertEquals(untouched, board4.shapeAt("rock-01"))

「積んだものが無いときに戻そうとしても、何も壊れません」

そして——戻した後に新しい一手をすれば、捨てた"もしも"はもう辿れないことも確かめた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 5. undo後に新しい操作を実行するとredo履歴が破棄される
val board5 = GardenBoard()
board5.place(Shape("stone-03", "石", 0, 0))

val history5 = CommandHistory()
history5.execute(MoveCommand(board5, "stone-03", 5, 0))   // A実行 → (5,0)
history5.undo()                                            // undo → (0,0)、redoにAが積まれている

history5.execute(MoveCommand(board5, "stone-03", 0, 7))    // 新しい操作Bを実行 → redoのAは破棄 → (0,7)
assertEquals(Shape("stone-03", "石", 0, 7), board5.shapeAt("stone-03"))

history5.redo()   // 破棄されたAは再適用されない → 状態はBのまま
assertEquals(Shape("stone-03", "石", 0, 7), board5.shapeAt("stone-03"))

history5.redo()   // もう一度呼んでも状態は変化しない(redoが実際に空であることの確認)
assertEquals(Shape("stone-03", "石", 0, 7), board5.shapeAt("stone-03"))

「一度戻した後に、別の一手をすれば、捨てたやり直し先は、二度と戻ってきません。何度redoを呼んでも、盤の姿は変わらないままです」

結界の中の模擬詠唱は、すべて、狙いどおりに通った。

怖くなくなった、意匠板

書き直された意匠板を、うちは受け取った。石も灯籠も、動かす・消すが、それぞれ戻せる一つの実体になっている。

「……これなら、思い切って動かしても、平気なのか」

「はい」とルーク。「動かしても、消しても、一手ずつ、確実に戻せます」

うちは、あの由緒ある灯籠を勘で戻した半日を思い出した。「勘で戻す必要が、もう無いんだな」

念のため、うちは念を押した。「これで、うちがどんな配置を試しても、絶対に大丈夫なのか」

ルークは正直に答えた。「戻す・やり直すが、ちゃんと動くことは確かめました。ただ、それぞれの操作の"戻し方"を、僕が間違えて書けば、その一手はやはり正しく戻りません。何でも自動で直る魔法、ではないんです」

「……なるほどな」とうちは笑った。「戻れる仕組みがある、ってだけで、十分だよ」

試しに、大胆に動かしてみる

うちは、少し迷ってから切り出した。「試しに、うちの手でやってみていいか」

意匠板の上で、これまで避けてきた大胆な配置換えを試す。最初の一手は、指先がまだ少し強張っていて、動かす手が一瞬迷った。だが、undoで戻せることを確かめると、二手目からは迷いが薄れていく。もう一段大胆にして、またundo。最後に、これまでで一番気に入った並びに、redoで戻す。

「……こんなに、気軽に試せるとは思わなかった」

指の強張りは、いつのまにか消えていた。

帰り支度をしながら、うちはふと、隅の年寄りに目をやった。あいかわらず、砂時計をひっくり返しては戻す、それだけをやっている。ルークに何を言うでもない。

工房を出る間際、ルークがその年寄りに軽く声をかけるのが、うちの耳に届いた。

「お師匠さま、また」

……お師匠さま。うちは、そこで初めて腑に落ちた。ああ、道理で。あの隠居の爺さんだと思っていた年寄りは、名の知れた大魔術師だったらしい。ルークが最後まで、誰の助けも借りず、自分の手だけで戻ってきた理由が、少し分かった気がした。

作業小屋へ戻ったうちは、まず意匠板の新しい仕組みを試しに動かしてみた。動かして、消して、戻して、またやり直して。手が、自然と大胆になっている自分に気づく。

——戻せる、というだけで、うちはこんなに自由になれるのか。

似たり寄ったりだった庭に、今度はうちの気に入った並びを、一つ足してみよう。


📖 見習い魔術師ルークの魔導書メモ(Luke’s Spellbook Notes)

  • 修復した呪文(クラス/モジュール名): Commandexecute/undoの約束)、MoveCommand/DeleteCommand/AddCommand(各操作の実体)、CommandHistory(undo/redoの履歴係)
  • 乱れの要因(アンチパターンの課題):
    • 意匠板のmove/removeが、盤のデータをその場で直接書き換えるだけで、動かす前・消す前の姿をどこにも控えていなかった。取り消し・履歴・再実行の仕組みが一切無く、依頼人(リン)は毎回、自分の記憶を頼りに逆算して手作業で戻すしかなく、実際に一度、勘で戻した位置がずれて依頼主に指摘された。
  • 過剰設計の反省(背伸びの過ち):
    • 操作の記録を一つの型(ActionLog)にまとめ、undo時に種類ごとの分岐で読み解けばいいと考え始めた。だが操作ごとに巻き戻しに必要な情報の"形"がまるで違うため、一つの記録型に無理に詰め込むと、使わないフィールドがどんどん増えて歪んでいくことに、自分で気づいて手を止めた。以前の"巨大な分岐"は、状態の組み合わせを網羅しようとして失敗したが、今回はそれとは別種——一つの入れ物に、形の違う戻し方を詰め込もうとする歪みだった。
  • 魔術の定石(学んだ設計パターン):
    • Command。操作そのものをexecute/undoを持つ一つの実体にし、各操作が自分の巻き戻しに必要な情報だけを持つようにした。履歴はundo用・redo用の2本のスタックで管理し、戻した後に新しい操作をすればやり直し履歴を捨てる、という定石も併せて実装した。
  • 模擬詠唱の記録(検証のポイント):
    • 各Commandのexecute/undoが正確に戻ること、任意の操作列をundo/redoできること、履歴が空のときのundoが安全に何もしないこと、undo後に新しい操作をするとredo履歴が破棄され、繰り返しredoを呼んでも状態が変化しないことを確認した。
  • ちなみに:
    • 今回のように「何をしたか」を覚えておくやり方をCommandと呼ぶ。一方、「状態そのもの」を丸ごと保存しておいて後で戻す考え方はMementoと呼ばれる別の定石で、何を戻すか=Command、どう戻すか=Memento、と役割が違う。undo/redoの履歴は、2本のスタックでなく、1本のリストとインデックスで管理するやり方でもよい。
  • リンの手が覚えたこと(今回、師匠マグナスは終始無言で、砂時計をひっくり返すばかりだった。だから、これは師の格言ではなく、うちがこの日持ち帰った一言):
    • 「戻せる、というだけで、うちはこんなに自由になれるのか。」
comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。