同じ骨格を、二度書き写す日々
私はトビアス。交易組合で記録係をしている。日々の取引を帳付けし、その日の記録から二種類の帳面を仕立てて回すのが仕事だ。数字が一桁でも合わないと、その日はもう落ち着かない。華々しさとは縁のない、ただ正確であることだけが取り柄の稼業だ。
作る帳面は二つある。ひとつは「卓上要覧」。組合の壁に貼り出す、品目ごとの一覧だ。誰が見ても、今日の売れ筋がひと目で分かるように、簡素に並べてある。もうひとつは「月次綴じ帳」。本部へ送る保管用の、正式な帳面だ。品目・数量・小計を項目立てし、末尾に合計を添える。
この二つは、作り方の途中まで、まったく同じだ。取引の記録を品目ごとに集めて足し合わせ、売れ筋の高い順に並べる——ここまでは寸分違わない。違うのは、最後の「見せ方」だけ。要覧はさらりと一行、綴じ帳は項目を立てて合計まで。それだけの違いだ。
私はこの二つを、別々の詠唱として書き持っていた。魔導式で言うところの、二つの関数だ。
| |
data class は、いくつかの値をひとまとめに運ぶための器だ。取引一件を Trade、集計した一行を LineItem として持つ。そして、帳面を作る詠唱が、こうだ。
| |
コードの中に、見慣れない言葉がいくつか出てきたので、先に触れておく。LinkedHashMap は、入れた順序を覚えている連想配列だ。品目を初めて見た順を保ってくれる。cur.copy(totalQty = …) は、cur(今ある集計行)の一部の値だけを差し替えた、新しい LineItem を作る書き方だ(data class に自動で備わる複製の仕組みで、元の値は書き換えない)。sortedByDescending { it.subtotal } は、小計の大きい順に並べ替える。StringBuilder は、文字列を少しずつ継ぎ足して組み立てるための入れ物だ。
見てのとおり、「集める」と「並べる」の段は、二つの詠唱で丸ごと同じだ。私は同じ文面を、二度書き写して持っている。
先月、これで肝を冷やした。売れ筋の並べ方を「小計の高い順」に統一する、ほんの小さな手直しを入れたときだ。私は綴じ帳のほうだけ直して、壁の要覧のほうを直し忘れた。半日のあいだ、壁の要覧と本部送りの綴じ帳とで、一番の売れ筋が食い違ったまま掲示されていた。合計の数字そのものは合っていたから大事には至らなかったが、本部から「要覧と綴じ帳で、一番の売れ筋が違って見えるが、どちらが正しいのか」と問い合わせが来て、私は冷や汗をかいた。
それ以来、集計や並べ替えの手順に手を入れるたび、私は必ず二つの詠唱を見比べて、寸分違わず同じに直すことに神経をすり減らしている。今は合っている。だが、「今は」でしかない。同じ工程を二箇所に抱えている限り、私はまた、必ず同じしくじりをやる。この綱渡りを、いつまで続けるのか。
薄くなって戻ってきた、よその綴じ帳
きっかけは、他の組合の記録係たちと集まる、月に一度の寄合の席だった。
隣の組合の記録係が、自分の帳面を卓に広げた。私は目を疑った。以前、その帳面は、あれこれの仕掛けが継ぎ足されて分厚く、入り組んでいたはずだ。それが、薄く、簡素になって戻ってきていた。中身が減ったわけじゃない。作りが、すっきりと片付いていた。
「どうしたんだ、これ」と私が訊くと、その記録係は言った。「丘のアカデミーの見習いに、見てもらったんだ。あの子は、仕掛けを足すんじゃない。要らない仕掛けを、省いて片付ける」。
私は、その言い回しに引っかかった。足すんじゃなく、省く。記録の稼業では珍しい話だ。たいていの直しは、何かを継ぎ足して分厚くなる。それに、と記録係は付け加えた。「あの見習いには、傍らでずっと黙って見ている、年寄りの師がいる。マグナスさま、というらしい」。
丘のルーク、そして師のマグナス。私はその二人の名を、帳面が薄くなって戻ってきたという事実といっしょに、しっかり覚えて帰った。翌日、私は二つの帳面の写しを抱えて、アカデミーの丘を上った。
工房は、宙に浮かぶ光の粒と、書物の匂いで満ちていた。奥の机で、若い見習いが手を動かしている。見ると、一枚の木の下敷き——型板を盤に置き、その上から同じ枠を紙に写し取っては、上端の銘、つまり見出しの一言だけを書き換えて、同じ枠の紙を何枚も刷っていた。
「枠は、一枚で足りる……変えるのは、ここだけだ」。そう手元で独りごちながら。
その傍ら、卓の隅に、年寄りが一人座っていた。卓上に色とりどりの魔石をいくつも広げ、一つずつ手元の小箱へ戻していく。広げては戻し、最後に一つだけを手のひらに残して、じっと眺める。それをまた繰り返している。私に目もくれない。あれがマグナスさまか、と私は得心した。噂のとおり、黙って見ているだけの人らしい。だが、あの黙った人は、いったい何のために、いつ口を開くのだろう。
私が声をかけると、見習いは紙を置いて顔を上げた。「あ……帳面の、ご相談ですか」。これがルークか。私は写しを卓に広げ、事情を語った。二つの帳面が、途中まで同じ工程を二度書きしていること。先月、片方だけ直して食い違わせた一件。そして、今も二箇所を見比べて揃える綱渡りが続いていること。
「この二つを、一つにまとめられないものか」と私は言った。「ただし、まとめた結果、前と寸分違わぬ物が刷れないと困る。本部に出す帳面だ。中身が一文字でも変われば、それは事故だ」。
ルークは写しをじっと見て、静かに言った。「集めて、並べる。ここまでが、二つでそっくり同じですね。違うのは、最後の出し方だけ」。そのとおりだ、と私は頷いた。
その拡張性、まだ誰も頼んでいない
ルークは、少し考えてから、机の上で新しい魔導式を組み始めた。その手つきが、途中から、妙に前のめりになった。
「出す部分を、差し替えできる形にしましょう」とルークは言った。「出し方そのものを一つの部品として切り出して、要覧用と綴じ帳用を、それぞれ別の部品にします。そうすれば……」。ルークの筆が走る。「この先、三つ目の書式でも、四つ目でも、どんな出し方が来ても後から差し込めるように、“登録簿"を用意しておけば——」。
私は、その手元を見て、内心身構えた。(また、ややこしくする気か。薄く片付けると聞いて来たんだが)。
ルークが書きかけていたのは、こういう形だった。
| |
interface は「こう呼べばこう応える」という約束の形だ。object は、この世に一つだけ存在する実体を作る書き方——ここでは、出し方を名前で登録して引き出す「登録簿」を一つだけ置こうとしている。ここは、ルークが書きかけて捨てた案だから、細部まで追わなくていい。大事なのは、この登録簿に何を差し込もうとしていたか、だ。
ルークは、登録簿に出し方を登録する行を書き進めた。要覧を登録し、綴じ帳を登録し、そして——三つ目を登録する行に筆を下ろしかけて、止まった。
「……この空欄」とルークは呟いた。「差し込む三つ目の出し方を、頼んだ人が、まだ誰もいない」。
工房が、しんとした。ルークは、書きかけの登録簿から手を離した。そして、机の端に置いてあった、さっき何枚も刷っていた型板を手に取り、指先で、とん、と軽く叩いた。
「……いや」。ルークは、自分で自分の言葉を引き取った。「要る帳面は、今、二つだけです。要覧と、綴じ帳。三つ目が来る保証もない。誰も頼んでいない出し方のために、差し込み口を先に作るのは——やめておきます」。
私は、その様子をただ見ていた。誰かに止められたわけじゃない。あの見習いは、自分で盛りかけて、自分で止めた。
要るものと、要らないもの
「ですが」と私は口を挟んだ。記録係として、確かめておかねばならないことがある。「この先、本当に三つ目の帳面が要るようになったら、どうするんだ。そのとき慌てて作り直すくらいなら、今のうちに差し込み口を用意しておくほうが、賢いんじゃないのか」。
ルークは首を振った。
「その時が来てから、部品を一つ足すだけで済むように、骨格のほうを整えておきます。まだ誰も頼んでいない書式のために、今、差し込み口まで作り込む必要はありません」。ルークは、卓の二枚の帳面を指した。「集める、並べる、出す——この順序は、今も、この先も変わりません。変わるのは、いちばん最後の"出し方"の一手だけです」。
そして、ルークは自分の言葉に、区切りをつけるように続けた。
「以前、あるお店の割引の詠唱を直したことがあります。あのときは、割引の"やり方"そのものを丸ごと差し替える必要が、本当にありました。季節の割引、会員の割引、日ごとに違うやり方を、まるごと入れ替えたかった。だから、やり方を部品にして差し替える定石——Strategy、つまり"やり方"を交換可能な部品として持たせるパターンが、あそこでは要ったんです」。
「でも今回は」とルークは、二枚の帳面をもう一度見た。「差し替えたい"やり方"が、そもそも無い。要覧は、いつでも要覧。綴じ帳は、いつでも綴じ帳。実行のたびに丸ごと入れ替える相手がいないのに、入れ替えるための仕掛けと、まだ空の登録簿を先に作る——それは、使わない道具を、先に鍛えておくようなものです」。
私の中で、何かがすっと腑に落ちた。「……なるほど。要るものと、要らないものを、分けたわけか」。今ある二つの重複を一つにまとめるのは、要る。まだ来ぬ三つ目のための仕掛けは、要らない。分けて考えれば、それだけのことだった。
その、私が納得した一拍のあとだった。卓の隅で、ずっと魔石を箱に戻していた老人が、ふと手を止めた。そして、手のひらに残した一つの魔石を眺めたまま、初めて口を開いた。
「“使わない"と、決められるようになったね」。
それだけだった。ルークは深く頭を下げた。マグナスさまは、また魔石に目を戻した。あの黙っていた人が、この日、口を開いたのは、その一言きりだった。答えを授けるのでもなく、ただ、ルークが自分で"使わない"と決めたことを、認めるためだけに。
「使わないと決めるのも、立派な判断なんだ」と、私は思わず呟いていた。私はてっきり、腕のいい魔術師とは、たくさんの道具を持ち、たくさん盛れる者のことだと思っていた。
骨格を、一枚に束ねる
「では、要る整理のほうをします」とルーク。二つの帳面が二度書きしている骨格を、一箇所にまとめる。ルークが取り上げたのは、さっき何枚も刷っていた、あの型板の考え方だった。
「集める、並べる——この骨格を、一枚の型に彫っておきます。そして、帳面ごとに変わる"出し方"だけを、あとから差し込む」。
| |
ルークは、聞き慣れない言葉を、ひとつずつ噛み砕いてくれた。
abstract class は、共通の骨格を持ちながら、一部を子の系統に委ねる「土台」の系統だ。それ単体では実体を作れない。abstract fun render(...) は、中身を書かずに、子の系統へ実装を義務づける「穴」のこと。子の系統は、この穴を override(=親が空けた穴を埋める印)で塞ぐ。protected を付けてあるので、この render は外の呼び出し側からは見えず、generate が内から呼ぶためだけに使われる。つまり、外から使う入口は generate ひとつだけになる。
「肝は、generate が既定で final になっていることです」とルーク。「Kotlin のメンバ関数は、open と付けない限り、子の系統から上書きできません。open を付ければ子が上書きできる——でも、あえて付けない。だから、集める→並べる→出す、という順序を、子が勝手に組み替えることはできない。骨格が、系統の決まりごととして守られるんです。上書きを許すのは、abstract を付けた render の一手だけ。変えていいのは、そこだけだと、はっきり決めておくんです」。
そして、変わる一手だけを実装した、二つの子の系統。
| |
「集める」と「並べる」は、もう親の ReportPipeline に一度だけ書いてある。子の BoardReport と LedgerReport は、それぞれの「出し方」だけを書けばいい。
このやり方を、Template Method と呼ぶのだそうだ——共通の骨格を親に固定し、変わる一手だけを子に委ねる定石だ。
私は、ふと引っかかった。「さっき君が捨てた案も、出し方を二つの部品に切り出していたな。あれと、今のこれは、何が違うんだ」。似たようなものに見える、と正直に言った。
「切り出す形は、確かに似ています」とルークは頷いた。「違うのは、出し方をどこに置くか、そして"集める・並べる"の共通の段をどう扱うか、です」。
そう言いながら、ルークは軽く指先を振った。 彼の指先からこぼれた淡い青紫の光が、卓上の薄暗い空間に広がり、きらめく魔力のスクロール(設計図)を二枚、並べるようにして描き出した。
左側に浮かび上がったのは、彼が盛りかけて自ら消し去った「Strategyと登録簿」の設計図だ。複雑に絡み合う黄金の光の矢印が、空の差し込み口へと頼りなく伸びている。 右側に浮かび上がったのは、たった今組み立てた「Template Method」の設計図だ。青紫の枠線で固定された堅牢な骨格(Pipeline)が、二つの帳面へ真っ直ぐと一本の背骨を通している。

ルークはまず、左側の複雑にうごめく光の束を指さした。 「さっきの案は、出し方を外から渡して、実行のたびに入れ替えられるようにする形——Strategy です。入れ替えを支えるために、差し込み口の登録簿まで要りました。でも、共通の段をどこに一度だけ置くかは、あの形だけでは決まらない」
そして、右側のすっきりとした光の骨格を指す. 「今度の Template Method は、出し方を子の系統に焼き付けて固定し、共通の"集める・並べる"は親に一度だけ置く。だから、二度書きが自然に一本化されます」
ルークは光のスクロールをそっと消し、区切るように言った。 「僕が捨てたのは、出し方を二つに分けること自体ではありません。捨てたのは、“実行のたびに入れ替える"仕掛けと、まだ誰も頼んでいない書式のための差し込み口——登録簿のほうです。入れ替えたい相手がいないのに、入れ替えの仕掛けを先に作る必要はない。だから今回は、骨格ごと固定して一部だけ変える Template Method のほうが、素直なんです」。
私は、あの半日の食い違いを思い出しながら訊いた。「これで、並べ方を直したくなったら、どうすればいい」。
「sort の一箇所を直すだけです」とルーク。「親に一度だけ書いてあるので、直せば要覧にも綴じ帳にも、同時に効きます。片方だけ、が起こりません」。
二箇所を見比べて揃える綱渡りが、そもそも存在しなくなる——ルークの言葉は、そういうことだった。
試し刷りで、二つの帳面を照らす
「本部へ綴じ帳を送る前に」とルークは言った。「結界の中で"試し刷り"をして、二つの帳面が、書き換える前と寸分ずれないかを確かめます」。
模擬詠唱——本番の前に、安全な結界の中で試しに詠唱して確かめる作法だ。ルークは、こんな取引記録を試しの元にした。
| |
薬草は二回に分かれているので、集めると数量4・小計40。鉄鉱は小計100、羊毛は小計40。これを小計の高い順に並べると、鉄鉱(100)、薬草(40)、羊毛(40)の順になる。
「薬草と羊毛は、どちらも小計40で同じだ。並びは、どうなる」と私は訊いた。細かいが、記録係にとっては大事な点だ。
「sortedByDescending は、値が同じもの同士は、元の並び順を崩しません。こういう並べ替えを"安定"と言います」とルーク。「薬草のほうが先に記録されているので、同じ40でも、薬草が羊毛より先に来ます。だから並びは、いつ試しても同じ——鉄鉱、薬草、羊毛です」。
まず、卓上要覧が期待どおりに刷れること。
| |
assertEquals は、狙いどおりの形と、実際に刷れた形とを照らし合わせる詠唱だ。次に、月次綴じ帳が、項目立てと末尾の合計まで含めて刷れること。
| |
合計は 100+40+40 で 180G。私がいちばん気にしていたのは、その次の一手だった。
| |
「これが、いちばん大事なところです」とルーク。「骨格を一つにまとめても、書き換える前の二本の詠唱と、まったく同じ物が刷れる。設計を整えただけで、中身は一文字も変えていない——それを、機械の目で照らして確かめます」。
前と寸分同じ。私が念を押した一点に、この一行がそのまま応えていた。設計を変えたが、帳面の中身は変わらない。だから事故にはならない。最後に、記録が空っぽのときにも、慌てず既定の形が返ること。
| |
取引が一件も無い日でも、要覧は見出しだけ、綴じ帳は見出しと合計0Gの形で、静かに返る。結界の中の試し刷りは、すべて狙いどおりに通った。
もう、二箇所を見比べなくていい
一本に束ねられた仕組みを、私は受け取った。骨格は、もう一箇所にしかない。
念のため、私は確かめた。「これで、間違って片方だけ直して、また食い違わせることは、無いんだな」。
ルークは、正直に答えた。「骨格が一つなので、“片方だけ"が起こりません。並べ方も、集め方も、直す場所は一箇所きりです。ただし——出し方、つまり render を僕が書き間違えれば、その帳面の見せ方は、やはり間違います。何でも自動で直る魔法ではないんです」。
「それで十分だよ」と私は笑った。「二箇所を揃える綱渡りが消えるだけで、私の肩の荷は、半分になる」。
組合へ戻った私は、さっそく試してみた。売れ筋が同じ小計で並んだときの扱いを、少しだけ変えてみようと思い立ち、骨格の並べ方を一箇所だけ手直しした。刷り直すと、壁の要覧も、本部送りの綴じ帳も、同時に、寸分違わず同じように変わった。
……二度と、二つを見比べて揃える必要が、無いのか。
指先に残っていた、あの綱渡りの緊張が、いつのまにか解けていた。私はこれまで、腕のいい記録係とは、どんな帳面の求めにも応えられるよう、あらかじめ手広く仕掛けを備えておく者だと思っていた。だが、あの見習いは、まだ誰も頼んでいない仕掛けを、迷いなく手放してみせた。
三つ目の帳面が本当に要る日が来たら、そのとき部品を一つ足せばいい。必要になってから、で間に合う。今日の私には、それがようやく、身にしみて分かった。
📖 見習い魔術師ルークの魔導書メモ(Luke’s Spellbook Notes)
- 修復した呪文(クラス/モジュール名):
ReportPipeline(骨格を固定する土台=テンプレートメソッドgenerate)、BoardReport/LedgerReport(“出し方"だけを実装した子の系統) - 乱れの要因(アンチパターンの課題):
- 卓上要覧と月次綴じ帳が「集める・並べる」の骨格を丸ごと二度書きで共有し、違うのは最後の出し方だけだった。現状は正しく動くが、集計や並べ方を直すたびに二箇所を揃えねばならず、片方を忘れれば二つの帳面が食い違う。実際に一度、売れ筋の並びが半日食い違い、本部からの問い合わせを招いた。
- 過剰設計の反省(=今回は"盛らなかった"判断):
- 出し方を差し替え可能な部品(Strategy)にし、“将来どんな書式でも差し込めるよう"登録簿(
FormatRegistry)まで作りかけた。だが、差し込む三つ目の書式を頼んだ者はまだ誰もおらず、実行のたびに丸ごと入れ替える相手もいない。まだ要らない拡張のための空の仕掛けだと気づき、自分で退けた。今ある二つの重複を一つに束ねる整理は要るが、まだ来ぬ需要の先取りは要らない——この線引き(YAGNI=必要になるまで作らない、という原則)を学んだ。
- 出し方を差し替え可能な部品(Strategy)にし、“将来どんな書式でも差し込めるよう"登録簿(
- 魔術の定石(学んだ設計パターン):
- Template Method。共通の骨格(集める→並べる→出す)を親の系統に固定し、変わる一手(出し方=
render)だけを子の系統に委ねる。Kotlin のメンバ関数は既定でfinal(openを付けない限り上書き不可)なので、骨格の順序は子が崩せない。三つ目の帳面が本当に要る時が来たら、子を一つ足すだけでよい(骨格には触れない)。
- Template Method。共通の骨格(集める→並べる→出す)を親の系統に固定し、変わる一手(出し方=
- 模擬詠唱の記録(検証のポイント):
- 卓上要覧・月次綴じ帳がそれぞれ正しく刷れること、書き換える前の二本の詠唱と寸分同じ物が刷れること(=設計を変えても中身は変わらない)、記録が空でも例外を出さず既定の形が返ることを確かめた。
- ちなみに:
- Template Method によく似た定石に Strategy がある。あちらは"やり方"を丸ごと別の部品に持たせて実行のたびに差し替える切り口で、“骨格ごと固定して一部だけ変える” Template Method とは狙いが違う。差し替えの必要が本当に生まれてから Strategy に移しても遅くない。「使わない」と決めるのも、立派な設計判断だ。
- 師匠マグナスの格言:
- 「“使わない"と、決められるようになったね」(この日、マグナスさまは終始、卓上の魔石を一つずつ小箱に戻し、最後の一つだけを手のひらに残していた。口を開いたのは、この一言きりだった)
