ランチの山が、ようやく越えたところだった。
厨房はまだ熱を持っていて、油のはぜる音と、出汁の湯気と、「お待ち!」の声が、まだ少しだけ重なっている。私はホールと厨房を行ったり来たりして、空いた皿を下げていた。注文は紙の伝票で入ってくる。シェフはそれを、カウンターの奥に立った金属の串——伝票刺しに、一枚ずつ刺していく。上から順に、刺さった伝票をさばいていく。
入り口近くの席のお客さんが、ふと顔を上げて言った。「すみません、さっき頼んだ唐揚げ定食、やっぱり焼き魚定食に変えられますか」
シェフは手を止めて、伝票刺しから一枚を抜いた。その人の伝票だ。さっと目を落として、「まだ手をつけてない。今なら戻せる」と言う。抜いた伝票をくしゃっと丸めて、新しい伝票に書き直し、また刺した。
その少し前、別のお客さんが「やっぱりさっきのは、なしで」と言ったときは、違った。シェフはちらりと厨房の奥を見て、「もう出しちまった。これは戻せねえ。作り直すしかねえな」と言った。
私はその二つの場面を、なんとなく覚えていた。戻せる注文と、戻せない注文がある。同じ「やっぱりやめます」でも、伝票を抜けば済むときと、もうどうにもならないときがある。
ピークが引けて、店が静かになった。中休みの、いちばん気の抜ける時間。引き戸が、遠慮がちに開いた。
本日の持ち込み
入ってきたのは、三十代半ばくらいの女性だった。きちんとした身なりで、入り口で一度立ち止まって、それから「お忙しいところ、すみません」と頭を下げた。
「職場の先輩に、設計で詰まったらここに行ってみろと言われて、来ました」。ゆっくり、言葉を選ぶような話し方だった。急いでいる様子はないけれど、ずっと考えてきたことがある、という顔をしている。
シェフは伝票刺しに残った最後の一枚を片付けてから、「座れ。何を持ち込んだ」と言った。
彼女はカウンターにノートPCを開いた。「飲食店の、レジ——会計のシステムを保守しています。店員さんが商品を打って、クーポン割引を入れて、という操作を、こういうメソッドで書いてきました」
| |
「add_item で商品を一行足して、apply_coupon で割引の行を足す。合計は、明細を全部足すだけです。最初は、これで何も問題ありませんでした」と彼女は言った。
私にはコードの中身は読めない。でも「商品を打つ」「割引を入れる」という言葉が、さっきまでの伝票のやりとりと重なって聞こえた。注文を一つずつ受けて、さばいていく。あれと同じことを、コードでやっているんだろうか。
「問題は、先月でした」と彼女は続けた。「店舗から、要望が来たんです」
やった手が、残らない
彼女は、指を折りながら要望を挙げていった。
「一つ。店員さんが打ち間違えたとき、その場で一手ずつ戻せるように。二つ。レジ締めのときに、『この会計で何をしたか』を店長が確認できるように。三つ。間違えて戻しすぎたら、やり直せるように」
「応急処置で、これを書きました」と、彼女はもう一つのメソッドを見せた。
| |
「直前の一手なら、これで戻せます。pop で、最後の明細を消すだけなので」。彼女はそこで、言葉を選ぶように少し黙った。「でも——三手前まで戻すのも、操作のログを出すのも、やり直しも、どう書けばいいのか、どうしても分からなくて。pop で戻せてはいるのに、それ以上が、まったく」
シェフは画面をしばらく眺めてから、void_last のあたりを指でとんとん、と叩いた。
「これは、最後の皿を下げてるだけだ。何を下げたかは、どこにも書いてない」
そう言って、シェフはさっき片付けた伝票刺しに目をやった。「さっき、客が『さっきの注文、変えてくれ』と言った。俺はどうした?」
彼女は少し考えて、「……伝票を、抜いていました」と答えた。
「そうだ。伝票が刺さってたから、抜いて戻せた。お前さんのコードには、その伝票がない。やった操作が、どこにも刺さってないんだ」
ここで、今回の問題の名前が出てきた。
unrecorded-operations(記録されない操作): 操作を手続きとして直接実行し、何を・どの順でやったかを残さない設計。結果(明細)は残るが、操作そのものが残らないため、取り消し・ログ・やり直しが原理的にできなくなる。
「add_item も apply_coupon も、操作を実行して、そのまま消えてる」とシェフは言った。「残るのは、出来上がった明細だけだ。『何をした手なのか』『どの順でやったか』は、どこにも残ってない。だから——後から戻す、見せる、やり直す、ができない」
彼女は静かにうなずいた。「最初は、操作はただ実行すればよかったんです。後から振り返る必要が、なかったので。要望が来て初めて、気づきました。私は『やった操作そのもの』を、どこにも持っていなかった」
私は、皿を拭く手が少し止まっていた。「やった操作が、残らない」。その言葉が、胸の奥に小さく引っかかった。前にここで見つけた「散らばり」とも、「入れ子の if」とも違う、別の形。でも、何が引っかかったのか、まだ言葉にならなかった。私は彼女の話に意識を戻した。
操作を、物にする
「やった操作を、一枚の伝票にする」とシェフは言って、コードを書き始めた。
まず、伝票そのものの「決まり」を作った。
| |
Command(コマンド): 操作(〜する、という一手)を、実行(execute)と取り消し(undo)を持つオブジェクトにまとめる技法。GoF の振る舞いパターンのひとつ。操作を「物」にすることで、後から積んだり、取り消したり、記録したりできるようになる。その共通の決まりを定めるのが、requires 'execute', 'undo', 'label' という **Operation という Role(役割)**だ。
「requires ってのは」とシェフは言った。「この役割を名乗るなら、この三つのメソッドを必ず持て、という約束だ。Perl は、メソッドがあるかどうかを後から確かめる言葉でな。この約束は、Operation を名乗るクラスを読み込んだその瞬間に、『お前、execute と undo と label を持ってるか?』と確かめる。持ってなきゃ、その場で止める」
次に、具体的な操作を一つ。「商品を打つ」だ。
| |
「AddItem という物にした」とシェフは言った。「中に、やること(execute)と、戻し方(undo)の両方が書いてある」。cart は、操作する相手——会計の中身だ。これを Receiver(受け手) と呼ぶ。実際に処理されるものだ。
同じ調子で、もう一つ。「クーポン割引」も伝票にした。
| |
AddItem も ApplyCoupon も、一つの操作を表すクラスだ。これを ConcreteCommand(具体コマンド) と呼ぶ。受け手への操作と、その戻し方を、セットで持っている。
彼女は、画面をじっと見ていた。それから、少し言いにくそうに口を開いた。
「……すみません、正直に言うと」と彼女は言った。「これ、add_item の中身を、ほとんどそのまま AddItem クラスに移しただけ、ですよね。しかも、undo を別に書いている分、コードはむしろ増えています。void_last で、直前は戻せていたんです。これで、何が良くなるんでしょうか」
批判ではなかった。本当に分からない、という顔だった。私も、同じことを思っていた。伝票という言い方はきれいだけれど、やっていることは、手続きをクラスに移しただけに見える。
シェフは、手を止めた。「いい疑いだ。行は増えた。それは認める」
それから、「さっきの要望、一つずつやってみるぞ」と言って、新しいクラスを書き始めた。
戻す、見せる、やり直す
シェフが書いたのは、伝票を管理する側だった。
| |
これが Invoker(実行役) だ。コマンドを受け取って実行し、実行した伝票を history に順に積んでいく。この積み重ねを 履歴スタック と呼ぶ。後入れ先出し——いちばん最近の手から、順に取り出す。
「一つずつ、見せるぞ」とシェフは言った。まず、操作を三つ積んでみせた。唐揚げ定食、クーポン、ドリンクだ。
| |
「まず、『この会計で何をしたか、見せろ』」。シェフは log_lines を呼んだ。
| |
「刺さってる伝票を、順に読むだけだ。Before は——操作を残してないから、これは作れない。明細はあっても、『どういう手でそうなったか』は、どこにもないからな」
彼女が、小さく息を呑んだ。
「次。『一手ずつ、何手でも戻す』」。シェフは undo_last を三回呼んだ。
| |
「刺さってる伝票を、上から順に抜くだけだ。一枚でも、三枚でも、好きなだけな。お前さんの void_last で、これができたか?」
「……いえ」と彼女は言った。「pop を繰り返せば、明細は消えます。でも、いま何手目を戻しているのか、何の操作を戻しているのか、分からなくなります」
「最後。『戻しすぎた、やり直せ』」。シェフは、今度は redo_last を三回呼んだ。
| |
「抜いた伝票を、捨てずに取っておく。だから、もう一度刺せる。一手でも、全部でも。三回やり直せば、さっきの通りだ。Before は pop で捨てちまうから、これもできない」
そして、シェフは OrderPad のコードを指でなぞった。
「見ろ。この OrderPad は、伝票が『商品追加』なのか『割引』なのか、一度も聞いてない。ただ、刺して、抜いて、読むだけだ。戻し方は、伝票自身が知ってる。だから、実行する役は、中身を知らなくていい」
「もし Before で undo を増やそうとしたら——」シェフは続けた。「あの void_last 一つが、全部の操作の戻し方を知る、でかい分岐になっていく。商品追加ならこう戻す、割引ならこう戻す、とな。Command は、それを一手ずつの伝票に分けた。戻し方を、操作自身に持たせたんだ」
私は、シェフが伝票を抜いたり刺したりする手つきを見ながら、自分の言葉にしてみた。
「つまり……手を動かすだけじゃなくて、動かした手を、一枚ずつ伝票にして、刺しておくんですね。そうすれば後から、刺さってる順に、抜いたり、読んだり、もう一度刺したりできる。手を動かすだけだと——戻すための手がかりが、何も残らない」
「そういうことだ」とシェフは短く返した。
その、「戻すための手がかりが、何も残らない」と自分で言った瞬間だった。II幕からずっと胸に引っかかっていたものが、急に、はっきりした形になった。
私の、在庫管理ツール。入荷と出荷を、ただ実行するだけのメソッドで書いた。半年前、入荷数を打ち間違えて、多すぎる数で出荷処理をしてしまった日のこと。取り消す手段はなくて、私は在庫の数を手で足し直した。合っているのかも分からないまま。何を、どう間違えたのかの記録は、一枚も残っていなかった。
私のあのコードには、戻すための伝票が、一枚もなかった——。
私はそれを、口には出さなかった。でも、もう、はっきりと分かっていた。
構造を絵にすると、こうなる。
classDiagram
class Operation {
<<Role>>
+execute()
+undo()
+label()
}
class AddItem {
+cart
+name
+price
}
class ApplyCoupon {
+cart
+amount
}
class OrderPad {
+history
+redo_stack
+do_operation(op)
+undo_last()
+redo_last()
+log_lines()
}
class Cart {
+lines
+total()
}
Operation <|.. AddItem : with
Operation <|.. ApplyCoupon : with
OrderPad o-- Operation : history
AddItem --> Cart : 操作する
ApplyCoupon --> Cart : 操作する
OrderPad が抱える history は Operation——つまり、商品追加でも、割引でも、同じ「伝票」として扱える。OrderPad は中身を知らない。だから、操作の種類に関係なく、「履歴・取り消し・やり直し・ログ」という横断的な仕事を、一か所で持てる。
戻せる手、戻せない手
「ただし」とシェフは、書く手を止めて言った。「戻せるのは、やり直しのきく手だけだ」
そう言って、さっきの営業中の話に戻した。
「まだ作ってない注文は、伝票を抜いて戻せた。だが、もう出しちまった料理は? 食っちまった料理は? 伝票を抜いても、現実は戻らねえ」
私は、ピークのときのあの二つの場面を思い出した。戻せた注文と、戻せなかった注文。
「コードも同じだ」とシェフは言った。「すでに出した料理、確定した会計、発行したレシート、送っちまった注文——そういう『戻せない手』は、undo を『お詫びの一品を出す』みたいな、新しい一手として書くか、そもそも取り消せないものとして扱う。これは、戻せるものを整理して、積めるようにする道具だ。戻せないものを、戻せるようにする魔法じゃねえ」
彼女が、ゆっくりうなずいた。「物理的に戻らないものは、Command でも戻らない。それは、はっきりさせておくべきですね」
「それと、もう一つ」とシェフは付け加えた。「戻し方に、変える前の値を覚えておかなきゃならん操作もある。たとえば『数量を三から五に変えた』のを戻すには、変える前の『三』を、伝票が覚えてなきゃならん。今回の追加と割引は、足した行を抜くだけだから簡単だが——操作によっては、戻すための情報を、伝票が持っておく必要がある。それはまた、別の話だ」
そういえば、と私は思った。前にここに来た人は、単品もコースも同じ顔にしていた(あれは Composite というらしい)。あれは、「モノ」を同じ顔にまとめる話だった。今回は、「コト」を物にする話だ。やったことを、手に取れる伝票にする。モノを同じ顔に、コトを物に。なんだか、対になっている気がした。
試食合格
コードを書き直して、テストを走らせた。
| |
要望の三つ——三手前まで戻す、操作ログ、やり直し——が、全部動いた。
「新しい操作を足すときも」と彼女が、自分から先を続けた。「execute と undo を持った伝票を、一つ書けばいいんですね。OrderPad は、触らなくていい」
「そうだ。ただし」とシェフは言った。「たとえば『数量変更』を戻すには、変える前の数を、その伝票が覚えておく必要があるぞ」
「……あ、確かに」と彼女は言った。「戻し方に、前の値がいる。戻せる手にも、手間の差があるんですね」
シェフは、伝票刺しを片付けながら言った。
「やった手は、一手ずつ伝票に残せ。残してあれば、戻せる手は戻せる。戻せない手も——どれが戻せないか、分かる」
「やった手を、残す」と彼女は繰り返した。「私はずっと、操作は実行すれば終わりだと思っていました。残しておく、という発想が、なかったんです」
来たときの、考え込むような硬さが、少しほどけて見えた。彼女はノートPCを閉じて、「先輩に、お礼を言わないと」と小さく笑った。
引き戸が閉まる音がした。
片付けをしながら、私は伝票刺しに残った、今日一日の伝票の束を、輪ゴムでまとめた。一枚一枚に、今日この店でやったことが書いてある。注文、変更、取り消し。全部、残っている。
そういえば、と思った。前にここで二回、私は閉店してから、ノートPCを開いて、自分のコードを確かめた。同じ言葉が三か所に散らばっているのを見つけた夜と、入れ子の if を見つけた夜。確かめて、やっと分かった。
でも今日は——開くまでもなかった。
シェフが伝票を三枚抜いて見せた、あの瞬間に、もう分かっていた。私の在庫ツール。入荷も、出荷も、ただ実行するだけ。あの誤出荷の日、私には戻すための伝票が、一枚もなかった。
「私も、同じミスを?」。ずっと胸にあったその問いに、今日、答えが出た気がする。やっぱり、私もだった。散らばりも、入れ子の if も、戻せない操作も。三つとも、私のあの小さなツールの中にある。
でも、不思議と、恥ずかしくはなかった。シェフはいつも言う。不味いのは、素材が悪いんじゃない。仕込みが足りなかっただけだ、と。私のコードも、たぶんそうだ。恥じゃない。まだ、仕込んでないだけ。
それに、もう一つ、気づいたことがあった。今日、私はコードを確かめてから「おかしい」と思ったんじゃない。シェフの手を見た、その瞬間に。確かめるより先に、舌が「おかしい」と言っていた。
——少しだけ、味が、分かるようになってきたのかもしれない。
シェフの仕込み工程表
| 問題(調理ミス) | 技法(パターン) | 効果(仕上がり) |
|---|---|---|
| 操作を手続きとして実行して消すだけ。やった操作が記録されない | Command(execute/undo を持つ Operation 伝票にする) | 操作が「物」になり、積む・読む・抜いて戻す・もう一度刺すができる |
void_last は直前を消すだけ。ログ・三手戻す・やり直しが原理的に作れない | Invoker(OrderPad)が履歴スタックに伝票を積む | 三手前まで戻す・操作ログ・やり直しが、種類に関係なく一か所でできる |
| 取り消しの戻し方を一か所に詰め込むと、全操作を知る巨大分岐になる | 戻し方(undo)を操作自身に持たせる | 実行役は中身を知らない。新しい操作は伝票を一枚足すだけ |
工程
Step 1: 操作の決まり(Operation)を Role で定義する
すべての操作が共通で持つべきものを Moo::Role で宣言する。requires で「この役割を名乗るクラスは、必ずこのメソッドを持つこと」を強制する。実行(execute)と取り消し(undo)、ログ用の一行(label)をセットにする。
| |
Step 2: 各操作を、伝票(ConcreteCommand)にする
一つの操作を、一つのクラスにする。「やること」を execute に、「自分の戻し方」を undo に、セットで書く。戻し方が操作自身に同居するのがポイントだ。
| |
Step 3: 実行役(Invoker)に履歴を持たせる
操作を受け取って実行し、実行した伝票を履歴スタックに積む。後入れ先出しで、undo_last は最後の伝票を抜いて undo を呼ぶ。戻した伝票は別のスタックに取っておけば、redo_last でやり直せる。実行役は、伝票が何の操作かを聞かない。
| |
Step 4: 呼び出し側は、実行役に操作を渡すだけにする
$pad->do_operation(AddItem->new(...)) のように、操作を作って実行役に渡す。取り消しもログもやり直しも、実行役に頼む。呼び出し側は、戻し方を知らなくていい。
Step 5: 戻せない手を、正直に扱う
物理的に戻らない操作(提供済み、会計確定、レシート発行、送信済みなど)は、undo を「補償する新しい一手」にするか、取り消し対象から外す。また、数量変更のように、戻すために変更前の値を覚えておく必要がある操作は、その値を伝票自身に持たせる。
シェフより
操作ってのは、手を動かすことだ。だが、動かしただけだと、消えちまう。後から「あれを戻せ」「あれを見せろ」「あれをもう一度」と言われても、手元に何も残ってない。お前さんのコードが詰まったのは、腕が悪いからじゃない。やった手を、残してなかっただけだ。
伝票にしろ。一手ごとに、やることと、戻し方を、一枚に書いて、刺しておく。そうすりゃ、積める、数えられる、読める、抜いて戻せる、もう一度刺せる。実行する側は、その伝票が何の料理かを知らなくていい。順番に管理するだけだ。
一つだけ、忘れるな。これは、戻せるものを戻せるようにする道具だ。戻せないものまで、戻せるようにはならねえ。出しちまった料理は、戻らねえ。どれが戻せて、どれが戻せないか——それをはっきりさせておくのも、立派な仕込みのうちだ。
