Featured image of post コードウィザード【KISS】一番小さな魔法で、足りる〜盛る誘惑を自ら退け、学んだ判断を過不足なく選んで組む〜

コードウィザード【KISS】一番小さな魔法で、足りる〜盛る誘惑を自ら退け、学んだ判断を過不足なく選んで組む〜

星祭りの予約を取り仕切る運営長ダリアは、空き枠・料金・状態・通知が一本に融けた予約呪文に悩む。見習い魔術師ルークは覚えた魔法を全部使いたい誘惑を自ら退け、必要な符だけを選び、KotlinのKISSで大依頼を驚くほど短い設計に収める。

四つの働きが、一本に

星祭りが近い。

わたくしはダリア。この街をあげての星祭りで、観覧席の手配を一手に預かる運営長を務めております。空を彩る星々を、一番よい席で見上げていただく。そのために、どなたを、どの席へ、いくらでお迎えするか——その段取りのすべてが、わたくしの肩に載っております。派手な仕事ではありません。ただ、祭りの晩に、誰ひとり席で困らずに済むこと。それだけを、長らく守ってまいりました。

観覧席の予約は、一本の大きな呪文で回しております。お客さまがお一組みえるたび、わたくしはこの呪文を頭から唱える。すると、四つのことが続けて起こります。まず、その席種にまだ空きがあるかを検める。次に、早割や団体割を織り込んで、お代を計算する。それから、仮のお預かりとして帳面に載せる。最後に、確認の触れをお送りする。空きを検めて、料金を出して、状態を記して、報せる。この四つが、一本の呪文のなかに、上から順に、ぜんぶ書き込まれております。

魔導式で申せば、こういう一つの魔法体——一つのクラスでございます。まず、予約にまつわる「もの」から。

1
2
3
4
5
6
7
8
// 観覧席の種類
enum class SeatKind { STANDARD, PREMIUM }

// 一件の予約(可変・状態を文字列で持つ)
class Booking(val name: String, val seatKind: SeatKind, val count: Int) {
    var status: String = "仮予約"   // "仮予約" / "確定" / "取消"
    var price: Int = 0
}

enum class は、取りうる種類を名前で出し切った型でございます。観覧席を、普通席(STANDARD)と特等席(PREMIUM)の二つに。Booking が一件のご予約で、お名前・席種・席数を持ち、そこへ状態(status)とお代(price)を添えます。状態は、いまは「仮予約」「確定」「取消」という文字(文字列)で持たせております。そして、予約の儀そのものが、こうです。

 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
// 祭りの予約台帳:空き判定・料金・状態・通知を一本のメソッドで抱える
class FestivalBox(private val capacity: Map<SeatKind, Int>) {
    private val bookingList = mutableListOf<Booking>()
    private val sentNotices = mutableListOf<String>()

    fun reserve(name: String, seatKind: SeatKind, count: Int, daysBefore: Int): Booking? {
        // ① 空き枠判定
        val used = bookingList.filter { it.seatKind == seatKind && it.status != "取消" }.sumOf { it.count }
        val cap = capacity[seatKind] ?: 0
        if (used + count > cap) return null           // 満席なら受けない

        // ② 料金計算(早割・団体割が直書き)
        val unit = if (seatKind == SeatKind.PREMIUM) 500 else 200
        var price = unit * count
        if (daysBefore >= 7) price = price * 9 / 10   // 早割 1割
        if (count >= 10) price = price - 300          // 団体割 300G 引き

        // ③ 状態+④ 通知
        val booking = Booking(name, seatKind, count)
        booking.price = price
        bookingList.add(booking)
        sentNotices.add("${name}様、${count}席お預かりしました(仮予約・${price}G)")
        return booking
    }

    fun confirm(b: Booking) { b.status = "確定"; sentNotices.add("${b.name}様、ご予約が確定しました") }
    fun cancel(b: Booking)  { b.status = "取消"; sentNotices.add("${b.name}様、ご予約を取り消しました") }
    fun bookings(): List<Booking> = bookingList.toList()
    fun notices(): List<String> = sentNotices.toList()
}

見慣れない書き方に、先に触れておきます。filter { … } は、条件に合うものだけを絞り込む書き方。sumOf { it.count } は、絞り込んだものの席数を足し上げる書き方でございます(it は、いま見ている一件ひとつを指す仮の呼び名)。ですから reserve のはじめの一行は、「取消でない同じ席種のご予約の、席数の合計」を数えている。capacity[seatKind] ?: 0?: は、定員が見つからなければ 0 とみなす、という意味です。daysBefore は、祭りまでの日数。七日以上前なら早割で一割引き、十席以上の団体なら三百グルド引き。

長らく、これで回ってまいりました。けれど祭りが年々大きくなるにつれ、種類の違う直しの注文が増えてまいりました。「早割の条件を変えたい」「団体割の額を見直したい」「触れの文面を改めたい」。そのたびにわたくしは、この一本の呪文の、真ん中に手を入れることになります。

先ごろ、肝を冷やしました。早割の条件を一つ直そうとして、料金のくだりに手を入れたときのこと。ところが、そのすぐ下に書いてあった団体割のほうまで、指が触れてしまった。気づかぬうちに、十席のお客さまへ、違うお代を提示していたのです。幸い、その日のうちに気づけましたが、背すじが冷えました。もう一度は、状態のときでした。「仮予約」と記すべきところを、うっかり「仮」とだけ書き損じ、判定が滑って、同じ席を二重に押さえかけた。どちらも大事には至りませんでしたが、料金を直したいだけ、状態を記したいだけなのに、なぜ、関わりのないところまで巻き添えにせねばならないのか。四つの働きが一本に融け合っていて、一部分だけを、この呪文はどうしても許してくれませんでした。

どこで訊いても、同じ名前

祭りの支度が進むほど、わたくしは妙なことに気づきました。

会場の設えに、頼れる職人を方々から集めます。灯りの油を仕入れる雑貨屋のご主人に、段取りの相談をしたとき。ふと、店の会計がずいぶん軽やかになったと聞きました。「丘の見習いに、直してもらったんだ」。連絡役をお願いしている冒険者ギルドの報せ係に、触れの手配を頼んだとき。「近ごろは、報せの出し忘れがなくなってね。あの丘の子のおかげ」。会場の庭を整える庭師に至っては、「試しの配置を、何度でもやり直せるようになった」と、手放しの喜びよう。

——丘の、見習い。訊く先、訊く先で、同じ名にたどり着く。ルーク、という名らしい。街の半分が、いつのまにか、あの一人の見習いの手で静かに支えられている。そう気づいたとき、わたくしは腹を決めました。星祭りの一番の難所、あの一本に膨れた予約呪文を、あの子に託してみよう、と。

翌日、わたくしは呪文の写しを抱え、アカデミーの丘を上りました。

工房は、思っていたよりずっと静かでした。机の上はきれいに片づき、床に、木を削った屑がわずかに散っている。窓から午後の光が差し込んで、その屑を淡く光らせておりました。奥の机で、若い見習いが、写しを覗き込むように背を丸めております。そしてもう一人。部屋の隅の椅子に、白髪の老人が腰かけ、小刀で木片を削っておりました。削っては、屑を払い、また削る。よけいなところを削ぎ落として、簡素な形だけを残していく。わたくしにはそれが、手なぐさみにしか見えず、気にも留めませんでした。

わたくしが声をかけると、見習いは顔を上げ、写しに目を落としたまま、静かに言いました。

「大きなお祭りの、要になる仕組みですね」

これがルークか、と思いました。噂よりも、ずっと落ち着いて見えます。わたくしは事情を語りました。四つの働きが一本に詰まっていること。早割を直そうとして団体割を巻き込んだこと。状態の書き損じで、危うく席を二重に押さえかけたこと。

ルークは写しをじっと辿り、こう言いました。「空きを検める、料金を出す、状態を記す、報せる——この四つの、違う働きが、一本の呪文に、上から順に、ぜんぶ融けています。だから、料金を直そうとすると、すぐ下の団体割に手が触れる」。

「そのとおりです」とわたくしは頷きました。「一部分だけを、この呪文は許してくれないのです」。

覚えた魔法を、全部

ルークの目が、写しの上で、きらりと動いたように見えました。

「これだけのお祭りなら——」

言うが早いか、彼は机の上に、次々と魔法陣を描き始めました。料金の“やり方”を丸ごと差し替える符。状態のひとつひとつを、別々の魔法体に仕立てる塔。報せを配るための、大きな伝令の網。取り消しと巻き戻しを司る符。予約を生み出す、生成の工房。そして、それら全部を束ねる、一枚の窓口。名を挙げながら、卓の上が、みるみる符で埋まっていきます。

わたくしは、その手際に見惚れました。噂に聞いた腕とは、これのことか。これだけの魔法を、一度に操れるとは。豪華な陣が、目の前で組み上がっていく。祭りの一番の難所も、この壮麗な魔法にかかれば——と、わたくしの胸は高鳴りました。期待は、そのとき頂点にありました。

隅の老人は、あいかわらず木片を削っております。ルークの華やかな手つきには目もくれず、ただ、削り屑を払っている。わたくしには、その静けさが、少し場違いにさえ見えました。

符を、広げて

ところが。

符を積み上げていくルークの手が、途中で、ふと止まりました。彼は、埋まっていく卓の上を見て、それから、何かを思い出したように、宙を見つめました。わたくしには、彼が胸の内で、遠い日の自分と向き合っているように見えました。むろん、その胸のうちは、わたくしには量りようもありません。ただ、彼はひとつ、小さく息をつきました。

そして——積み上げるのをやめ、いま並べた符を、卓の上に、一枚ずつ広げ直し始めたのです。

「待ってください」とルーク。「この祭りに、どれが要るのか——一枚ずつ、確かめます」。

彼は符を一枚手に取り、しばらく眺めては、要るか要らぬかを、声に出して選り分けていきました。

わたくしは、気になっていたことを尋ねました。「取り消しの符は。お客さまが、気を変えられたら」。

ルークは、その符を手のひらに載せたまま、答えました。「これは、取り消しややり直しを、いくつも遡って辿りたいときの符です。……今日は、頼まれていません。要る日が来たら、その符を、また表に返します」。そう言って、伏せました。

「報せは、いくつも要るのではありませんか」とわたくし。祭りには、あちこちへの連絡がつきものです。

「これは、報せる先がいくつも増えて、そのたびに繋ぎ直すのが辛いときの符です」。彼はその大きな網を、たたむように伏せました。「今日の受け手は、確認の触れ、ひとつきり。網は要りません」。

「では、料金は」とわたくしは食い下がりました。「割引を、あの差し替えの符で切り出さないのですか」。

ルークは、その符を少し掲げて言いました。「これは、料金の“やり方”そのものを、丸ごと別のものに差し替えたいときの符。……差し替える相手が、今いないのです。ルールは、早割と団体割の二つきりで、動かない。それなら、関数ひとつで足ります」。

生成の工房も、束ねる窓口も、彼はわたくしが問う前に、まとめて一掃きに伏せました。「予約を作る場所は、一つきり。束ねるほど、奥は多くありません」。卓の上の符が、みるみる減っていきます。

残ったのは、ほんの数枚でした。状態を型で出し切る符と、値を書き換えない、不変の器の符。

わたくしは、思わず漏らしました。「……え、それだけ、ですか。あんなに、符があったのに」。豪華な陣を待っていた分だけ、拍子抜けしたのは、正直なところです。

ルークは、少し困ったように、けれどはっきりと言いました。「強い符ほど偉い、ではないのです。今日の火種が、一番小さな魔法で消せるか——それを、先に。使うほど、いま読みにくくなる符は、今日は毒なのです」。

こうして「一番小さな道具で足りるか」を先に問い、過不足のない設計に留める心得を、KISS(Keep It Simple, Stupid=必要以上に複雑にしない、という設計原則)と呼ぶのだそうです。わたくしには、まだ半信半疑でした。あれだけの符を伏せて、本当に、祭りが回るのか。

料金は、関数ひとつ

ルークは、残した符から、まず料金にとりかかりました。

「料金は、素直な関数ひとつにします」。彼は羊皮紙に、短い呪文を書きました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 料金は、素直な関数ひとつ。早割・団体割は、ここだけが知る。
fun priceOf(seatKind: SeatKind, count: Int, daysBefore: Int): Int {
    val unit = when (seatKind) {
        SeatKind.STANDARD -> 200
        SeatKind.PREMIUM -> 500
    }
    var price = unit * count
    if (daysBefore >= 7) price = price * 9 / 10   // 早割
    if (count >= 10) price -= 300                 // 団体割
    return price
}

when (seatKind) { … } は、値によって行き先を選ぶ書き方。ここでは席種で単価を選び、あとは早割・団体割を順に織り込むだけ。お代の計算を知っているのは、もうこの関数一つきりです。

「前に、あるお店の割引を直したことがあります」とルークは、手を止めずに言いました。「あのときは、割引の“やり方”そのものを、季節のもの、常連のもの、と丸ごと差し替えたかった。だから、やり方を一枚ずつ符に切り出したのです」。

やり方の一群を部品に切り出し、丸ごと差し替えられるようにする——その定石を、Strategy と呼ぶのだそうです。ルークは、伏せた料金の符を、ちらりと見やりました。「でも今日は、差し替えたい“やり方”が、そもそも無い。早割は、いつでも早割。団体割は、いつでも団体割。丸ごと入れ替える相手がいないのに、入れ替えの仕掛けと、それを納める登録簿まで先に作る——それは、使わない道具を、先に鍛えておくようなものです」。

わたくしにも、そこは腑に落ちました。差し替えるあてもないのに、差し替えの棚だけ立派に構える。たしかに、妙な話です。

状態は、型で出し切る

次に、ルークは状態にとりかかりました。

「状態を、文字で持つのはやめます。取りうる姿を、型で出し切ります」。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 予約の状態は、取りうる姿を型で出し切る(素の object)
sealed interface Status {
    object Tentative : Status   // 仮予約
    object Confirmed : Status   // 確定
    object Cancelled : Status   // 取消
}

// 網羅 when:状態を足すと、コンパイラが「枝が足りない」と数え漏れを見張る
fun labelOf(status: Status): String = when (status) {
    Status.Tentative -> "仮予約"
    Status.Confirmed -> "確定"
    Status.Cancelled -> "取消"
}

sealed interface は、「取りうる姿は、この一族だけ」と型で封じる宣言です。仮予約・確定・取消。この三つきり、と決めてしまう。それぞれの状態は object——たった一つきりの実体で、名のついた目印のようなもの——で表します。

「文字で持っていたころは」とルーク。「“仮予約”と書くところを“仮”と書き損じても、誰も止めてくれませんでした。でも、型で出し切ってしまえば、Status.Tentative という決まった目印しか置けない。書き損じは、そもそも起こせなくなります」。

そして、labelOfwhen を指しました。「取りうる姿を出し切ってあるので、三つの枝をすべて書けば、『その他』を受け止める枝は要りません。裏を返せば——もし将来、状態を一つ足して、この枝を書き足すのを忘れたら、魔法を編む段で『枝が一つ足りない』と、はじかれます。数え漏れを、コンパイラが見張ってくれるのです」。

わたくしが、あの状態の書き損じで冷や汗をかいた一件に、この一手が、そのまま応えていました。

さらにルークは、予約そのものを、書き換えられない器に改めました。

1
2
3
4
5
6
7
8
9
// 予約は不変の器。状態変更は copy で新しい姿を生む。id で一意に識別する
data class Reservation(
    val id: Int,
    val name: String,
    val seatKind: SeatKind,
    val count: Int,
    val price: Int,
    val status: Status,
)

data class は、いくつかの値をひとまとめに運ぶ器。頭に付いた val は、一度置いたら書き換えない印です。「状態が変わるときは、この器を書き換えるのではなく、copy で、状態だけを差し替えた新しい器を生みます」とルーク。id は、どのご予約かを見分けるための、一意の番号です。

台帳は、短く

最後に、四つの働きを収める台帳を、ルークは組み上げました。驚いたのは、その短さです。

 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
// 予約台帳:短い。通知は "受け手ひとつ" を受け取るだけ
class FestivalLedger(
    private val capacity: Map<SeatKind, Int>,
    private val notify: (String) -> Unit = {},
) {
    private val reservationById = LinkedHashMap<Int, Reservation>()   // id → 予約(登録順を保つ)
    private var seq = 0                                               // 発番用の連番

    fun reserve(name: String, seatKind: SeatKind, count: Int, daysBefore: Int): Reservation? {
        val used = reservationById.values
            .filter { it.seatKind == seatKind && it.status != Status.Cancelled }
            .sumOf { it.count }
        if (used + count > (capacity[seatKind] ?: 0)) return null     // 満席
        seq += 1
        val r = Reservation(seq, name, seatKind, count, priceOf(seatKind, count, daysBefore), Status.Tentative)
        reservationById[r.id] = r
        notify("${name}様、${count}席お預かりしました(仮予約・${r.price}G)")
        return r
    }

    fun confirm(id: Int) = transition(id, Status.Confirmed, "ご予約が確定しました")
    fun cancel(id: Int) = transition(id, Status.Cancelled, "ご予約を取り消しました")

    // 状態変更は copy で新しい姿に差し替える(取消は集計から外れ、空きが戻る)
    private fun transition(id: Int, to: Status, msg: String) {
        val r = reservationById[id] ?: return
        reservationById[id] = r.copy(status = to)
        notify("${r.name}様、$msg")
    }

    fun reservations(): List<Reservation> = reservationById.values.toList()
}

見慣れない書き方に、触れておきます。台帳の頭にある notify: (String) -> Unit は、「文字を一つ受け取って、それで何かする」という届け先を、一つだけ外から受け取る書き方です。LinkedHashMap は、入れた順を覚えている連想配列で、ご予約を id で引けるようにしています。

「報せは、この notify に、確認の触れを一つ渡すだけ」とルーク。「今日の受け手は、ひとつきり。だから、届け先を一つ受け取るだけで足ります」。

報せを発する側と受け取る側を切り離し、受け手をいくつも疎らに束ねて通知する——その定石を、Observer と呼ぶのだそうです。「でも、それは受け手が幾つも増えて、繋ぎ直しが辛くなってからでいい」とルーク。今日は、その大きな網を、彼は広げませんでした。

空きの計算は、取消でないご予約の席数を足すだけ。料金は priceOf に任せ、状態は Status に出し切ってある。取り消せば、その予約は集計から外れ、空きが戻ります。四つの働きが、それぞれ一番小さな形で収まり、台帳はただ、それらを短く繋いでいるだけでした。

ルークが杖を軽く振ると、羊皮紙の上に、新しく整えられた関係がすっきりとした光の図となって浮かび上がりました。

「ご覧ください。これが、今回の新しい台帳の姿です」

KISS Reservation System Structure

「見ての通り、台帳である FestivalLedger は予約の登録と状態遷移だけを管理しています。料金の計算は独立した priceOf 関数に委ね、状態は安全な Status という型に閉じ込めました。お互いが小さく、独立しているからこそ、どこを直しても他を巻き込むことはありません」

わたくしは、浮かび上がる光の図を見つめました。あの一本に融けていた複雑な呪文が、まるでよく整理された棚のように、きれいに分かれて並んでいる。複雑な魔法陣の組み合わせではなく、ただこれだけのシンプルな並び。

結界で、試す

「本物の台帳を祭りに出す前に」とルークは言いました。「安全な結界のなかで、この仕組みが狙いどおりに働くかを、一つずつ検めます」。

魔術師は、本番の前に、結界のなかで魔導式を試すのだそうです。模擬詠唱、と申しました。まず、料金が、境目まできっちり合うこと。

1
2
3
4
5
assertEquals(720, priceOf(SeatKind.STANDARD, 4, 7))   // 早割: 200*4=800 → 720
assertEquals(800, priceOf(SeatKind.STANDARD, 4, 6))   // 6日前は早割なし → 800
assertEquals(1700, priceOf(SeatKind.STANDARD, 10, 6)) // 団体: 2000 → 1700
assertEquals(1800, priceOf(SeatKind.STANDARD, 9, 6))  // 9席は団体割なし → 1800
assertEquals(1500, priceOf(SeatKind.STANDARD, 10, 7)) // 早割+団体: 2000→1800→1500

assertEquals は、狙いどおりの額と、実際に出た額を照らし合わせる詠唱です。七日前ちょうどで早割が効き、六日前では効かない。十席ちょうどで団体割が効き、九席では効かない。境目のすぐ両側を、一つずつ検めてあります。

次に、空き枠。定員ちょうどまでは受け、超えれば断ること。

1
2
3
4
val ledger = FestivalLedger(mapOf(SeatKind.STANDARD to 20, SeatKind.PREMIUM to 6))
assertEquals("アイナ", ledger.reserve("アイナ", SeatKind.STANDARD, 12, 0)?.name)
assertEquals("ベル",   ledger.reserve("ベル",   SeatKind.STANDARD, 8,  0)?.name) // 計20で定員ちょうど
assertEquals(null,     ledger.reserve("カイ",   SeatKind.STANDARD, 1,  0))       // 超過は満席で null

reserve は、満席だと予約できず null を返します。だから ?.name ——「予約できたらそのお名前を、満席で null ならそのまま null を」——という書き方で、お名前を取り出しています。

そして、状態の移り変わりと、取り消せば空きが戻ること。

1
2
3
4
5
val hall = FestivalLedger(mapOf(SeatKind.STANDARD to 20))
val r = hall.reserve("満員", SeatKind.STANDARD, 20, 0)!!
assertEquals(null, hall.reserve("待ち", SeatKind.STANDARD, 1, 0))   // 満席
hall.cancel(r.id)                                                   // 取り消すと
assertEquals("空いた", hall.reserve("空いた", SeatKind.STANDARD, 1, 0)?.name) // 空きが戻る

!! は「ここは必ず予約できる」とみなして中身を取り出す書き方です(もし null なら、その場で止まります)。取り消すと、その席は集計から外れる。だから、さっきまで満席だった枠に、新しいお客さまが、ちゃんと入れます。

わたくしが、いちばん念を押したかったのは、その次でした。奥を組み替えても、書き換える前の一本の呪文と、寸分同じ祭りになること。同じ三組のお客さまを、書き換える前の呪文(box)と、組み替えた台帳(ledger。送った触れは notices に集めます)へ、同じ順で通します。そして、出てきたものを、照らし合わせます。

1
2
3
4
5
6
7
8
// 同じ操作の列を、Before(一本の呪文)と After(組み替えた台帳)の両方に流し、
// 出てきた触れと、各予約の「お名前/席種/席数/お代/状態」が、寸分一致することを照らす。
// ※ 中の作りは違う(文字列の状態か、型か)ので、"観測できる出し物" だけを突き合わせる。
assertEquals(box.notices(), notices)                              // 触れが寸分一致
assertEquals(
    box.bookings().map  { "${it.name}/${it.seatKind}/${it.count}/${it.price}/${it.status}" },
    ledger.reservations().map { "${it.name}/${it.seatKind}/${it.count}/${it.price}/${labelOf(it.status)}" },
)

「これが、いちばん大事なところです」とルーク。「奥を組み替えても、書き換える前の呪文と、まったく同じ触れが飛び、同じお代になる。お客さまから見えるものは、一つも変えていない——それを、機械の目で照らして確かめます」。前と寸分同じ。わたくしが心配していた一点に、この照らしが、そのまま応えていました。ちなみに、満席のときに断る(null を返す)ふるまいも、書き換える前と後で、まったく同じ判断をします。設計を整えただけで、中身は変えていないのです。

結界のなかの模擬詠唱は、すべて、狙いどおりに通りました。

直す場所が、一つになった

整え直された予約の仕組みを、わたくしは受け取りました。あの豪華な符の塔は、跡形もありません。手元にあるのは、短い関数と、いくつかの小さな器と、驚くほど短い台帳だけ。

「本当に、これで、当日さばけるのですか」とわたくしは尋ねました。あの拍子抜けが、まだ胸に残っていたのです。

「はい」とルーク。「試しに、いちばん気になっていたところを、ご自分の手で」。

わたくしは、おそるおそる、早割の額を一つ、直してみました。priceOf の、たった一箇所。刷り直すと——普通席も、特等席も、団体のお客さまも、すべてのお代が、一度に、正しく変わりました。あの、料金を直すと隣の団体割まで巻き込んだ、冷や汗の一件。それが起こる場所が、もう、無いのです。直す場所が、一つになっている。

拍子抜けは、いつのまにか、感嘆に変わっておりました。豪華さは、どこにもない。けれど、わたくしがいちばん恐れていたことが、静かに消えている。

ルークは、正直に付け加えました。「直す場所が、一つになりました。ただ——その一つを、わたしが書き違えれば、お代はやはり間違います。型は、状態の書き損じは封じます。けれど、料金の中身の正しさまでは、保証しません。消えるのは、“どこに響くか読めない”という、あの不安のほうです」。

「それで、十分でございます」とわたくしは申しました。「どこに響くか分からないのが、いちばん恐ろしかったのですから」。

わたくしが帰り支度をしておりますと、それまでずっと木片を削っていた隅の老人が、ふと手を止めました。削り上げた、飾りのない簡素な木片を、卓にことりと置く。そして、ルークのほうへ——問いを、ひとつ、投げました。詰めるでも、試すでもない。まるで、答えを知りたがっているような、静かな声で。

「ルーク。お前さんは、あの符の、どれを“使わぬ”かを——どうやって、決めたのだね」。

わたくしは、はっとしました。この問いの重さ。教え諭すのではなく、たしかめている。ああ、この方が、あの子のお師匠さまか。名の知れた大魔術師マグナスさまが、いま、ご自分のお弟子に、教えを請うている。

ルークは、少し考えてから、静かに答えました。「……その符を使ったら、いま目の前の仕組みが、読みやすくなるのか、読みにくくなるのか。それを、先に」。

マグナスさまは、小さくうなずくと、また削り屑を、ふっと払いました。

わたくしは、丘を下りました。星祭りの晩、観覧席はひとつの淀みもなく埋まり、早割の見直しも、席種の追加も、そのたびに、たった一箇所を直すだけで済みました。あの一本に融けて、どこに響くか読めなかった呪文は、もうどこにもありません。派手な魔法ではなかった。ただ、祭りの晩に、誰ひとり席で困らずに済む。わたくしがずっと守ってきたその一点が、こんなにも軽く、手のなかに収まっていることが、なにより嬉しゅうございました。


〔エピローグ〕使わなかった符

床に落ちた、師の削り屑を、ひとつ拾い上げる。よけいなところを、ぜんぶ削ぎ落とした、簡素な形。

はじめて依頼を受けた日のことを、思い出していた。あのときの僕は、雑貨屋の会計を前に、「あらゆる割引に備えます」と胸を張った。習いたての高位魔法を、片端から積み上げて、砦のように盛って——止まれなかった。強い魔法ほど、偉い。そう、信じて疑わなかった。

あれから、たくさんの依頼を受けた。足すな、と学んだ。分けろ、と学んだ。相手を変えず、間に通訳を立てろ。操作に、名前を与えろ。ばらばらに覚えたつもりだった。でも、今日、卓に符を広げて、一枚ずつ伏せていくうちに、気づいてしまった。あの一つひとつの判断は、ぜんぶ、たった一つの問いだったのだ。——これは、一番単純な形で、足りるか。

今日、僕は、あの符の山を前にして、誰に問われるより先に、自分にそれを問えた。師の声を借りずに。あの頃、砦を積んで止まれなかった手が、今日は、自分で止まった。

師が、「どうやって決めたのか」と訊いた。手を引く側だった人が、僕の選び方を、知りたがった。それが、どういうことなのか——うまくは言えない。ただ、ひとつだけ、自分の言葉にできた気がする。——一番強い魔法を選べる者より、使わずに済ませる魔法を一番多く知っている者のほうが、遠くまで行ける。

伏せた符は、捨てたのではない。要る日が来たら、また表に返す。ただ、今日は要らなかった。それだけのことだ。削り屑を、そっと机に置いた。


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

  • 修復した呪文(クラス/モジュール名): priceOf(料金の関数)/Status(sealed interface)+labelOf(状態と網羅 when)/Reservation(data class・不変)/FestivalLedger(短い台帳。通知は受け手ひとつのコールバック)
  • 乱れの要因(アンチパターンの課題):
    • 星祭りの予約が、空き枠判定・料金・状態・通知という、変更する理由の違う四つの働きを、一本の reserve に上から順に抱えていた。現状は正しく動くが、料金の直書きゆえ早割を直すと団体割を巻き込み、文字列の状態ゆえ「仮予約」を「仮」と書き損じて枠を二重に押さえかけた。一部分だけを直したいのに、どこに響くか読めなかった。
  • 過剰設計の反省(=今回は“盛らなかった”判断):
    • 大きな依頼に高揚し、Strategy/State/Observer/Command/Factory/Facade を一度に積みかけた(初めての依頼で継承の砦を盛った、あの日の再来)。だが符を卓に広げ、“今日の火種に要るか”で一枚ずつ選り分け、ほとんどを伏せた。却下は「嫌い」ではなく「差し替える相手が今いない/受け手は一つ/頼まれていない」——今の要件を読みやすくしない抽象は、今日は毒だ、という一つの物差しによる。
  • 魔術の定石(学んだ設計判断):
    • KISS。残したのは、料金=関数(Strategy の軽い形。差し替える相手が無いのでクラス群と登録簿は不要)、状態=sealed interface+網羅 when(State のクラス群は過剰。効能は「文字列の書き損じを型で封じる」+「数え漏れをコンパイルが見張る」)、不変=data classcopy、通知=受け手ひとつのコールバック(Observer の疎結合の考えは活かし、基盤の重さは持ち込まない)。合成して、四つの働きの大依頼が、驚くほど短く収まった。パターンは目的でなく、手段。
  • 模擬詠唱の記録(検証のポイント):
    • 料金の境目(早割 7日/6日・団体 10席/9席)、空き枠(定員ちょうど・満席は null)、状態遷移(取消で空きが戻る)、そして書き換える前と寸分同じであること(=観測できる出し物=触れ・お名前・席種・席数・お代・状態ラベルの列で照合。中の作りは比べない)を確かめた。満席で断るふるまいも Before/After で同じ(=バグ直しではなく、設計だけを変えた証)。状態を一つ足せば、網羅 when がコンパイルで漏れを指す。
  • 師匠マグナスの格言:
    • 「お前さんは、あの符の、どれを“使わぬ”かを——どうやって、決めたのだね」(この日マグナスさまは終始、木片を削って簡素な形だけを残していた。口を開いたのは、答えでなく、この問いひとつ。師が、弟子の選び方を知りたがった日として、記しておく)
comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。