Featured image of post コードウィザード【YAGNI】その拡張性、まだ誰も頼んでいない〜二つの帳面の重複をTemplate Methodで一本化し、抽象化は差分が育ってから〜

コードウィザード【YAGNI】その拡張性、まだ誰も頼んでいない〜二つの帳面の重複をTemplate Methodで一本化し、抽象化は差分が育ってから〜

交易組合の記録係トビアスは、二つの帳面が集計の骨格をコピペで抱え、片方だけ直すと数字の見え方が食い違う綱渡りに悩む。見習い魔術師ルークは差し替え可能な仕組みと登録簿を盛りかけて自ら退け、KotlinのTemplate Methodで骨格を一本化する。使わない勇気を描く。

同じ骨格を、二度書き写す日々

私はトビアス。交易組合で記録係をしている。日々の取引を帳付けし、その日の記録から二種類の帳面を仕立てて回すのが仕事だ。数字が一桁でも合わないと、その日はもう落ち着かない。華々しさとは縁のない、ただ正確であることだけが取り柄の稼業だ。

作る帳面は二つある。ひとつは「卓上要覧」。組合の壁に貼り出す、品目ごとの一覧だ。誰が見ても、今日の売れ筋がひと目で分かるように、簡素に並べてある。もうひとつは「月次綴じ帳」。本部へ送る保管用の、正式な帳面だ。品目・数量・小計を項目立てし、末尾に合計を添える。

この二つは、作り方の途中まで、まったく同じだ。取引の記録を品目ごとに集めて足し合わせ、売れ筋の高い順に並べる——ここまでは寸分違わない。違うのは、最後の「見せ方」だけ。要覧はさらりと一行、綴じ帳は項目を立てて合計まで。それだけの違いだ。

私はこの二つを、別々の詠唱として書き持っていた。魔導式で言うところの、二つの関数だ。

1
2
3
4
5
// 一件の取引記録
data class Trade(val item: String, val quantity: Int, val unitPrice: Int)

// 集計後の一行(品目ごとの小計)
data class LineItem(val item: String, val totalQty: Int, val subtotal: Int)

data class は、いくつかの値をひとまとめに運ぶための器だ。取引一件を Trade、集計した一行を LineItem として持つ。そして、帳面を作る詠唱が、こうだ。

 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
34
35
36
37
38
39
40
// 壁に貼る「卓上要覧」を作る
fun generateBoardReport(trades: List<Trade>): String {
    // 1. 集める:品目ごとに数量と小計を合算
    val grouped = LinkedHashMap<String, LineItem>()
    for (t in trades) {
        val cur = grouped[t.item]
        grouped[t.item] =
            if (cur == null) LineItem(t.item, t.quantity, t.quantity * t.unitPrice)
            else cur.copy(totalQty = cur.totalQty + t.quantity, subtotal = cur.subtotal + t.quantity * t.unitPrice)
    }
    // 2. 並べる:小計の高い順(売れ筋が上に来る)
    val lines = grouped.values.sortedByDescending { it.subtotal }
    // 3. 出す:卓上要覧の形(ここだけが綴じ帳と違う)
    val sb = StringBuilder("◇ 卓上要覧 ◇\n")
    for (l in lines) sb.append("${l.item}  ${l.totalQty}${l.subtotal}G\n")
    return sb.toString()
}

// 本部へ送る「月次綴じ帳」を作る
fun generateLedgerReport(trades: List<Trade>): String {
    // 1. 集める:品目ごとに数量と小計を合算   ← generateBoardReport と一字一句同じ
    val grouped = LinkedHashMap<String, LineItem>()
    for (t in trades) {
        val cur = grouped[t.item]
        grouped[t.item] =
            if (cur == null) LineItem(t.item, t.quantity, t.quantity * t.unitPrice)
            else cur.copy(totalQty = cur.totalQty + t.quantity, subtotal = cur.subtotal + t.quantity * t.unitPrice)
    }
    // 2. 並べる:小計の高い順   ← ここも同じ
    val lines = grouped.values.sortedByDescending { it.subtotal }
    // 3. 出す:月次綴じ帳の形(項目立て+末尾に合計。違うのはここだけ)
    val sb = StringBuilder("=== 月次綴じ帳 ===\n")
    var total = 0
    for (l in lines) {
        sb.append("品目: ${l.item} / 数量: ${l.totalQty} / 小計: ${l.subtotal}G\n")
        total += l.subtotal
    }
    sb.append("--- 合計: ${total}G ---\n")
    return sb.toString()
}

コードの中に、見慣れない言葉がいくつか出てきたので、先に触れておく。LinkedHashMap は、入れた順序を覚えている連想配列だ。品目を初めて見た順を保ってくれる。cur.copy(totalQty = …) は、cur(今ある集計行)の一部の値だけを差し替えた、新しい LineItem を作る書き方だ(data class に自動で備わる複製の仕組みで、元の値は書き換えない)。sortedByDescending { it.subtotal } は、小計の大きい順に並べ替える。StringBuilder は、文字列を少しずつ継ぎ足して組み立てるための入れ物だ。

見てのとおり、「集める」と「並べる」の段は、二つの詠唱で丸ごと同じだ。私は同じ文面を、二度書き写して持っている。

先月、これで肝を冷やした。売れ筋の並べ方を「小計の高い順」に統一する、ほんの小さな手直しを入れたときだ。私は綴じ帳のほうだけ直して、壁の要覧のほうを直し忘れた。半日のあいだ、壁の要覧と本部送りの綴じ帳とで、一番の売れ筋が食い違ったまま掲示されていた。合計の数字そのものは合っていたから大事には至らなかったが、本部から「要覧と綴じ帳で、一番の売れ筋が違って見えるが、どちらが正しいのか」と問い合わせが来て、私は冷や汗をかいた。

それ以来、集計や並べ替えの手順に手を入れるたび、私は必ず二つの詠唱を見比べて、寸分違わず同じに直すことに神経をすり減らしている。今は合っている。だが、「今は」でしかない。同じ工程を二箇所に抱えている限り、私はまた、必ず同じしくじりをやる。この綱渡りを、いつまで続けるのか。

薄くなって戻ってきた、よその綴じ帳

きっかけは、他の組合の記録係たちと集まる、月に一度の寄合の席だった。

隣の組合の記録係が、自分の帳面を卓に広げた。私は目を疑った。以前、その帳面は、あれこれの仕掛けが継ぎ足されて分厚く、入り組んでいたはずだ。それが、薄く、簡素になって戻ってきていた。中身が減ったわけじゃない。作りが、すっきりと片付いていた。

「どうしたんだ、これ」と私が訊くと、その記録係は言った。「丘のアカデミーの見習いに、見てもらったんだ。あの子は、仕掛けを足すんじゃない。要らない仕掛けを、省いて片付ける」。

私は、その言い回しに引っかかった。足すんじゃなく、省く。記録の稼業では珍しい話だ。たいていの直しは、何かを継ぎ足して分厚くなる。それに、と記録係は付け加えた。「あの見習いには、傍らでずっと黙って見ている、年寄りの師がいる。マグナスさま、というらしい」。

丘のルーク、そして師のマグナス。私はその二人の名を、帳面が薄くなって戻ってきたという事実といっしょに、しっかり覚えて帰った。翌日、私は二つの帳面の写しを抱えて、アカデミーの丘を上った。

工房は、宙に浮かぶ光の粒と、書物の匂いで満ちていた。奥の机で、若い見習いが手を動かしている。見ると、一枚の木の下敷き——型板を盤に置き、その上から同じ枠を紙に写し取っては、上端の銘、つまり見出しの一言だけを書き換えて、同じ枠の紙を何枚も刷っていた。

「枠は、一枚で足りる……変えるのは、ここだけだ」。そう手元で独りごちながら。

その傍ら、卓の隅に、年寄りが一人座っていた。卓上に色とりどりの魔石をいくつも広げ、一つずつ手元の小箱へ戻していく。広げては戻し、最後に一つだけを手のひらに残して、じっと眺める。それをまた繰り返している。私に目もくれない。あれがマグナスさまか、と私は得心した。噂のとおり、黙って見ているだけの人らしい。だが、あの黙った人は、いったい何のために、いつ口を開くのだろう。

私が声をかけると、見習いは紙を置いて顔を上げた。「あ……帳面の、ご相談ですか」。これがルークか。私は写しを卓に広げ、事情を語った。二つの帳面が、途中まで同じ工程を二度書きしていること。先月、片方だけ直して食い違わせた一件。そして、今も二箇所を見比べて揃える綱渡りが続いていること。

「この二つを、一つにまとめられないものか」と私は言った。「ただし、まとめた結果、前と寸分違わぬ物が刷れないと困る。本部に出す帳面だ。中身が一文字でも変われば、それは事故だ」。

ルークは写しをじっと見て、静かに言った。「集めて、並べる。ここまでが、二つでそっくり同じですね。違うのは、最後の出し方だけ」。そのとおりだ、と私は頷いた。

その拡張性、まだ誰も頼んでいない

ルークは、少し考えてから、机の上で新しい魔導式を組み始めた。その手つきが、途中から、妙に前のめりになった。

「出す部分を、差し替えできる形にしましょう」とルークは言った。「出し方そのものを一つの部品として切り出して、要覧用と綴じ帳用を、それぞれ別の部品にします。そうすれば……」。ルークの筆が走る。「この先、三つ目の書式でも、四つ目でも、どんな出し方が来ても後から差し込めるように、“登録簿"を用意しておけば——」。

私は、その手元を見て、内心身構えた。(また、ややこしくする気か。薄く片付けると聞いて来たんだが)。

ルークが書きかけていたのは、こういう形だった。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 「出し方」を差し替え可能な部品として切り出す
interface ReportFormat {
    fun render(lines: List<LineItem>): String
}
class BoardFormat : ReportFormat { override fun render(lines: List<LineItem>): String = "" /* 要覧の形(書きかけ) */ }
class LedgerFormat : ReportFormat { override fun render(lines: List<LineItem>): String = "" /* 綴じ帳の形(書きかけ) */ }

// 「どんな出し方でも後から差し込める」登録簿
object FormatRegistry {
    private val formats = mutableMapOf<String, ReportFormat>()
    fun register(name: String, format: ReportFormat) { formats[name] = format }
    fun get(name: String): ReportFormat? = formats[name]
}

interface は「こう呼べばこう応える」という約束の形だ。object は、この世に一つだけ存在する実体を作る書き方——ここでは、出し方を名前で登録して引き出す「登録簿」を一つだけ置こうとしている。ここは、ルークが書きかけて捨てた案だから、細部まで追わなくていい。大事なのは、この登録簿に何を差し込もうとしていたか、だ。

ルークは、登録簿に出し方を登録する行を書き進めた。要覧を登録し、綴じ帳を登録し、そして——三つ目を登録する行に筆を下ろしかけて、止まった。

「……この空欄」とルークは呟いた。「差し込む三つ目の出し方を、頼んだ人が、まだ誰もいない」。

工房が、しんとした。ルークは、書きかけの登録簿から手を離した。そして、机の端に置いてあった、さっき何枚も刷っていた型板を手に取り、指先で、とん、と軽く叩いた。

「……いや」。ルークは、自分で自分の言葉を引き取った。「要る帳面は、今、二つだけです。要覧と、綴じ帳。三つ目が来る保証もない。誰も頼んでいない出し方のために、差し込み口を先に作るのは——やめておきます」。

私は、その様子をただ見ていた。誰かに止められたわけじゃない。あの見習いは、自分で盛りかけて、自分で止めた。

要るものと、要らないもの

「ですが」と私は口を挟んだ。記録係として、確かめておかねばならないことがある。「この先、本当に三つ目の帳面が要るようになったら、どうするんだ。そのとき慌てて作り直すくらいなら、今のうちに差し込み口を用意しておくほうが、賢いんじゃないのか」。

ルークは首を振った。

「その時が来てから、部品を一つ足すだけで済むように、骨格のほうを整えておきます。まだ誰も頼んでいない書式のために、今、差し込み口まで作り込む必要はありません」。ルークは、卓の二枚の帳面を指した。「集める、並べる、出す——この順序は、今も、この先も変わりません。変わるのは、いちばん最後の"出し方"の一手だけです」。

そして、ルークは自分の言葉に、区切りをつけるように続けた。

「以前、あるお店の割引の詠唱を直したことがあります。あのときは、割引の"やり方"そのものを丸ごと差し替える必要が、本当にありました。季節の割引、会員の割引、日ごとに違うやり方を、まるごと入れ替えたかった。だから、やり方を部品にして差し替える定石——Strategy、つまり"やり方"を交換可能な部品として持たせるパターンが、あそこでは要ったんです」。

「でも今回は」とルークは、二枚の帳面をもう一度見た。「差し替えたい"やり方"が、そもそも無い。要覧は、いつでも要覧。綴じ帳は、いつでも綴じ帳。実行のたびに丸ごと入れ替える相手がいないのに、入れ替えるための仕掛けと、まだ空の登録簿を先に作る——それは、使わない道具を、先に鍛えておくようなものです」。

私の中で、何かがすっと腑に落ちた。「……なるほど。要るものと、要らないものを、分けたわけか」。今ある二つの重複を一つにまとめるのは、要る。まだ来ぬ三つ目のための仕掛けは、要らない。分けて考えれば、それだけのことだった。

その、私が納得した一拍のあとだった。卓の隅で、ずっと魔石を箱に戻していた老人が、ふと手を止めた。そして、手のひらに残した一つの魔石を眺めたまま、初めて口を開いた。

「“使わない"と、決められるようになったね」。

それだけだった。ルークは深く頭を下げた。マグナスさまは、また魔石に目を戻した。あの黙っていた人が、この日、口を開いたのは、その一言きりだった。答えを授けるのでもなく、ただ、ルークが自分で"使わない"と決めたことを、認めるためだけに。

「使わないと決めるのも、立派な判断なんだ」と、私は思わず呟いていた。私はてっきり、腕のいい魔術師とは、たくさんの道具を持ち、たくさん盛れる者のことだと思っていた。

骨格を、一枚に束ねる

「では、要る整理のほうをします」とルーク。二つの帳面が二度書きしている骨格を、一箇所にまとめる。ルークが取り上げたのは、さっき何枚も刷っていた、あの型板の考え方だった。

「集める、並べる——この骨格を、一枚の型に彫っておきます。そして、帳面ごとに変わる"出し方"だけを、あとから差し込む」。

 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
// 骨格(集める→並べる→出す)を一箇所に束ねるテンプレート
abstract class ReportPipeline {

    // 三工程の「呼び出し順序」を固定する詠唱。
    // Kotlin のメンバ関数は、open を付けない限り既定で final(=子の系統から上書きできない)。
    // だから「集める→並べる→出す」の骨格は、どの帳面でも決して崩れない。
    fun generate(trades: List<Trade>): String {
        val collected = collect(trades) // 1. 集める(共通・ここに一度だけ書く)
        val ordered = sort(collected)   // 2. 並べる(共通・ここに一度だけ書く)
        return render(ordered)          // 3. 出す(ここだけ帳面ごとに変わる)
    }

    // 共通の段:どの帳面でも同じなので、親に一度だけ置く(=二度書きの解消)
    private fun collect(trades: List<Trade>): List<LineItem> {
        val grouped = LinkedHashMap<String, LineItem>()
        for (t in trades) {
            val cur = grouped[t.item]
            grouped[t.item] =
                if (cur == null) LineItem(t.item, t.quantity, t.quantity * t.unitPrice)
                else cur.copy(totalQty = cur.totalQty + t.quantity, subtotal = cur.subtotal + t.quantity * t.unitPrice)
        }
        return grouped.values.toList()
    }

    private fun sort(lines: List<LineItem>): List<LineItem> = lines.sortedByDescending { it.subtotal }

    // 変わる一手:帳面ごとに違う「出し方」だけを、子の系統に委ねる(abstract=子が必ず埋める穴)
    protected abstract fun render(lines: List<LineItem>): String

    // ※ もし将来、本当に三つ目の帳面が要るようになったら、
    //   この ReportPipeline を継ぐ子を一つ足して render を書くだけでよい(骨格には一切触れない)。
    //   その時が来てから足せば十分。今は要らない。
}

ルークは、聞き慣れない言葉を、ひとつずつ噛み砕いてくれた。

abstract class は、共通の骨格を持ちながら、一部を子の系統に委ねる「土台」の系統だ。それ単体では実体を作れない。abstract fun render(...) は、中身を書かずに、子の系統へ実装を義務づける「穴」のこと。子の系統は、この穴を override(=親が空けた穴を埋める印)で塞ぐ。protected を付けてあるので、この render は外の呼び出し側からは見えず、generate が内から呼ぶためだけに使われる。つまり、外から使う入口は generate ひとつだけになる。

「肝は、generate が既定で final になっていることです」とルーク。「Kotlin のメンバ関数は、open と付けない限り、子の系統から上書きできません。open を付ければ子が上書きできる——でも、あえて付けない。だから、集める→並べる→出す、という順序を、子が勝手に組み替えることはできない。骨格が、系統の決まりごととして守られるんです。上書きを許すのは、abstract を付けた render の一手だけ。変えていいのは、そこだけだと、はっきり決めておくんです」。

そして、変わる一手だけを実装した、二つの子の系統。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 卓上要覧:出し方だけを実装
class BoardReport : ReportPipeline() {
    override fun render(lines: List<LineItem>): String {
        val sb = StringBuilder("◇ 卓上要覧 ◇\n")
        for (l in lines) sb.append("${l.item}  ${l.totalQty}${l.subtotal}G\n")
        return sb.toString()
    }
}

// 月次綴じ帳:出し方だけを実装(末尾に合計を付す)
class LedgerReport : ReportPipeline() {
    override fun render(lines: List<LineItem>): String {
        val sb = StringBuilder("=== 月次綴じ帳 ===\n")
        var total = 0
        for (l in lines) {
            sb.append("品目: ${l.item} / 数量: ${l.totalQty} / 小計: ${l.subtotal}G\n")
            total += l.subtotal
        }
        sb.append("--- 合計: ${total}G ---\n")
        return sb.toString()
    }
}

「集める」と「並べる」は、もう親の ReportPipeline に一度だけ書いてある。子の BoardReportLedgerReport は、それぞれの「出し方」だけを書けばいい。

このやり方を、Template Method と呼ぶのだそうだ——共通の骨格を親に固定し、変わる一手だけを子に委ねる定石だ。

私は、ふと引っかかった。「さっき君が捨てた案も、出し方を二つの部品に切り出していたな。あれと、今のこれは、何が違うんだ」。似たようなものに見える、と正直に言った。

「切り出す形は、確かに似ています」とルークは頷いた。「違うのは、出し方をどこに置くか、そして"集める・並べる"の共通の段をどう扱うか、です」。

そう言いながら、ルークは軽く指先を振った。 彼の指先からこぼれた淡い青紫の光が、卓上の薄暗い空間に広がり、きらめく魔力のスクロール(設計図)を二枚、並べるようにして描き出した。

左側に浮かび上がったのは、彼が盛りかけて自ら消し去った「Strategyと登録簿」の設計図だ。複雑に絡み合う黄金の光の矢印が、空の差し込み口へと頼りなく伸びている。 右側に浮かび上がったのは、たった今組み立てた「Template Method」の設計図だ。青紫の枠線で固定された堅牢な骨格(Pipeline)が、二つの帳面へ真っ直ぐと一本の背骨を通している。

Strategyと登録簿による過剰設計(却下案)とTemplate Methodによる骨格固定(採用案)の魔法回路対比図

ルークはまず、左側の複雑にうごめく光の束を指さした。 「さっきの案は、出し方を外から渡して、実行のたびに入れ替えられるようにする形——Strategy です。入れ替えを支えるために、差し込み口の登録簿まで要りました。でも、共通の段をどこに一度だけ置くかは、あの形だけでは決まらない」

そして、右側のすっきりとした光の骨格を指す. 「今度の Template Method は、出し方を子の系統に焼き付けて固定し、共通の"集める・並べる"は親に一度だけ置く。だから、二度書きが自然に一本化されます」

ルークは光のスクロールをそっと消し、区切るように言った。 「僕が捨てたのは、出し方を二つに分けること自体ではありません。捨てたのは、“実行のたびに入れ替える"仕掛けと、まだ誰も頼んでいない書式のための差し込み口——登録簿のほうです。入れ替えたい相手がいないのに、入れ替えの仕掛けを先に作る必要はない。だから今回は、骨格ごと固定して一部だけ変える Template Method のほうが、素直なんです」。

私は、あの半日の食い違いを思い出しながら訊いた。「これで、並べ方を直したくなったら、どうすればいい」。

sort の一箇所を直すだけです」とルーク。「親に一度だけ書いてあるので、直せば要覧にも綴じ帳にも、同時に効きます。片方だけ、が起こりません」。

二箇所を見比べて揃える綱渡りが、そもそも存在しなくなる——ルークの言葉は、そういうことだった。

試し刷りで、二つの帳面を照らす

「本部へ綴じ帳を送る前に」とルークは言った。「結界の中で"試し刷り"をして、二つの帳面が、書き換える前と寸分ずれないかを確かめます」。

模擬詠唱——本番の前に、安全な結界の中で試しに詠唱して確かめる作法だ。ルークは、こんな取引記録を試しの元にした。

1
2
3
4
5
6
val trades = listOf(
    Trade("薬草", 3, 10), // 小計 30
    Trade("鉄鉱", 2, 50), // 小計 100
    Trade("薬草", 1, 10), // 薬草に加算 → 数量4・小計40
    Trade("羊毛", 5, 8),  // 小計 40
)

薬草は二回に分かれているので、集めると数量4・小計40。鉄鉱は小計100、羊毛は小計40。これを小計の高い順に並べると、鉄鉱(100)、薬草(40)、羊毛(40)の順になる。

「薬草と羊毛は、どちらも小計40で同じだ。並びは、どうなる」と私は訊いた。細かいが、記録係にとっては大事な点だ。

sortedByDescending は、値が同じもの同士は、元の並び順を崩しません。こういう並べ替えを"安定"と言います」とルーク。「薬草のほうが先に記録されているので、同じ40でも、薬草が羊毛より先に来ます。だから並びは、いつ試しても同じ——鉄鉱、薬草、羊毛です」。

まず、卓上要覧が期待どおりに刷れること。

1
2
3
4
5
6
// 1. 卓上要覧が、狙いどおりの形で刷れる
val expectedBoard = "◇ 卓上要覧 ◇\n" +
    "鉄鉱  2点  100G\n" +
    "薬草  4点  40G\n" +
    "羊毛  5点  40G\n"
assertEquals(expectedBoard, BoardReport().generate(trades))

assertEquals は、狙いどおりの形と、実際に刷れた形とを照らし合わせる詠唱だ。次に、月次綴じ帳が、項目立てと末尾の合計まで含めて刷れること。

1
2
3
4
5
6
7
// 2. 月次綴じ帳が、合計付きで狙いどおりに刷れる
val expectedLedger = "=== 月次綴じ帳 ===\n" +
    "品目: 鉄鉱 / 数量: 2 / 小計: 100G\n" +
    "品目: 薬草 / 数量: 4 / 小計: 40G\n" +
    "品目: 羊毛 / 数量: 5 / 小計: 40G\n" +
    "--- 合計: 180G ---\n"
assertEquals(expectedLedger, LedgerReport().generate(trades))

合計は 100+40+40 で 180G。私がいちばん気にしていたのは、その次の一手だった。

1
2
3
// 3. 書き換える前(二本の詠唱)と、寸分同じ物が刷れる
assertEquals(generateBoardReport(trades), BoardReport().generate(trades))
assertEquals(generateLedgerReport(trades), LedgerReport().generate(trades))

「これが、いちばん大事なところです」とルーク。「骨格を一つにまとめても、書き換える前の二本の詠唱と、まったく同じ物が刷れる。設計を整えただけで、中身は一文字も変えていない——それを、機械の目で照らして確かめます」。

前と寸分同じ。私が念を押した一点に、この一行がそのまま応えていた。設計を変えたが、帳面の中身は変わらない。だから事故にはならない。最後に、記録が空っぽのときにも、慌てず既定の形が返ること。

1
2
3
// 4. 記録が空でも、例外を出さず既定の形が返る
assertEquals("◇ 卓上要覧 ◇\n", BoardReport().generate(emptyList()))
assertEquals("=== 月次綴じ帳 ===\n--- 合計: 0G ---\n", LedgerReport().generate(emptyList()))

取引が一件も無い日でも、要覧は見出しだけ、綴じ帳は見出しと合計0Gの形で、静かに返る。結界の中の試し刷りは、すべて狙いどおりに通った。

もう、二箇所を見比べなくていい

一本に束ねられた仕組みを、私は受け取った。骨格は、もう一箇所にしかない。

念のため、私は確かめた。「これで、間違って片方だけ直して、また食い違わせることは、無いんだな」。

ルークは、正直に答えた。「骨格が一つなので、“片方だけ"が起こりません。並べ方も、集め方も、直す場所は一箇所きりです。ただし——出し方、つまり render を僕が書き間違えれば、その帳面の見せ方は、やはり間違います。何でも自動で直る魔法ではないんです」。

「それで十分だよ」と私は笑った。「二箇所を揃える綱渡りが消えるだけで、私の肩の荷は、半分になる」。

組合へ戻った私は、さっそく試してみた。売れ筋が同じ小計で並んだときの扱いを、少しだけ変えてみようと思い立ち、骨格の並べ方を一箇所だけ手直しした。刷り直すと、壁の要覧も、本部送りの綴じ帳も、同時に、寸分違わず同じように変わった。

……二度と、二つを見比べて揃える必要が、無いのか。

指先に残っていた、あの綱渡りの緊張が、いつのまにか解けていた。私はこれまで、腕のいい記録係とは、どんな帳面の求めにも応えられるよう、あらかじめ手広く仕掛けを備えておく者だと思っていた。だが、あの見習いは、まだ誰も頼んでいない仕掛けを、迷いなく手放してみせた。

三つ目の帳面が本当に要る日が来たら、そのとき部品を一つ足せばいい。必要になってから、で間に合う。今日の私には、それがようやく、身にしみて分かった。


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

  • 修復した呪文(クラス/モジュール名): ReportPipeline(骨格を固定する土台=テンプレートメソッド generate)、BoardReport / LedgerReport(“出し方"だけを実装した子の系統)
  • 乱れの要因(アンチパターンの課題):
    • 卓上要覧と月次綴じ帳が「集める・並べる」の骨格を丸ごと二度書きで共有し、違うのは最後の出し方だけだった。現状は正しく動くが、集計や並べ方を直すたびに二箇所を揃えねばならず、片方を忘れれば二つの帳面が食い違う。実際に一度、売れ筋の並びが半日食い違い、本部からの問い合わせを招いた。
  • 過剰設計の反省(=今回は"盛らなかった"判断):
    • 出し方を差し替え可能な部品(Strategy)にし、“将来どんな書式でも差し込めるよう"登録簿(FormatRegistry)まで作りかけた。だが、差し込む三つ目の書式を頼んだ者はまだ誰もおらず、実行のたびに丸ごと入れ替える相手もいない。まだ要らない拡張のための空の仕掛けだと気づき、自分で退けた。今ある二つの重複を一つに束ねる整理は要るが、まだ来ぬ需要の先取りは要らない——この線引き(YAGNI=必要になるまで作らない、という原則)を学んだ。
  • 魔術の定石(学んだ設計パターン):
    • Template Method。共通の骨格(集める→並べる→出す)を親の系統に固定し、変わる一手(出し方=render)だけを子の系統に委ねる。Kotlin のメンバ関数は既定で finalopen を付けない限り上書き不可)なので、骨格の順序は子が崩せない。三つ目の帳面が本当に要る時が来たら、子を一つ足すだけでよい(骨格には触れない)。
  • 模擬詠唱の記録(検証のポイント):
    • 卓上要覧・月次綴じ帳がそれぞれ正しく刷れること、書き換える前の二本の詠唱と寸分同じ物が刷れること(=設計を変えても中身は変わらない)、記録が空でも例外を出さず既定の形が返ることを確かめた。
  • ちなみに:
    • Template Method によく似た定石に Strategy がある。あちらは"やり方"を丸ごと別の部品に持たせて実行のたびに差し替える切り口で、“骨格ごと固定して一部だけ変える” Template Method とは狙いが違う。差し替えの必要が本当に生まれてから Strategy に移しても遅くない。「使わない」と決めるのも、立派な設計判断だ。
  • 師匠マグナスの格言:
    • 「“使わない"と、決められるようになったね」(この日、マグナスさまは終始、卓上の魔石を一つずつ小箱に戻し、最後の一つだけを手のひらに残していた。口を開いたのは、この一言きりだった)
comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。