Featured image of post コードウィザード【Facade】分けたのに、もっと絡まった〜手順で切る誘惑を退け、責務で分けてFacadeの窓口で隠す〜

コードウィザード【Facade】分けたのに、もっと絡まった〜手順で切る誘惑を退け、責務で分けてFacadeの窓口で隠す〜

職人組合の受付係セイラの入会の儀は、登録・初期設定・通知・計測を一本の神クラスが抱え、一部を直すと隣まで巻き込む。見習い魔術師ルークは手順で分ける誘惑を退け、KotlinのFacadeと責務分割で奥を役目ごとに分け、段取りを窓口ひとつに預ける。

一部分だけを、この呪文は許してくれない

私はセイラ。職人組合で、入会の受付係をしている。新しい職人が組合の門をくぐるたびに、その人を迎え入れる「入会の儀」を、一手に取り仕切るのが私の仕事だ。窓口に立って、日がな一日、人を見ている。派手さはないけれど、手続きが滞りなく回って、新入りが安心した顔で工房へ向かっていく——それを見届けるのが、私のささやかな誇りだった。

入会の儀は、一本の大きな呪文でできている。新入りが一人来るたびに、私はこの呪文を頭から唱える。すると、四つのことが順番に起きる。まず、名簿にその人を載せて会員の札を発番する。次に、見習い部屋と、その職種に合った初期道具を割り当てる。それから、歓迎の触れ状を送る。最後に、入会の統計を本部の簿冊へ計上する。登録して、設定して、通知して、計測する。この四つが、一本の呪文の中に、上から順に、ぜんぶ書き込まれている。

魔導式で言えば、こういう一つの魔法体——一つのクラスだ。

1
2
3
4
5
6
7
8
// 入会の申込者
data class Applicant(val name: String, val craft: String)

// 登録された会員
data class Member(val id: String, val name: String, val craft: String)

// 見習いの初期設定(部屋番号・初期道具)
data class Profile(val memberId: String, val room: Int, val tools: List<String>)

data class は、いくつかの値をひとまとめに運ぶための器だ。入会を申し込む人を Applicant、登録された会員を Member、割り当てた部屋と道具を Profile として持つ。そして、入会の儀そのものが、こうだ。

 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
41
42
43
// 入会の儀のすべて(登録・初期設定・通知・計測)を、一つのクラス・一つのメソッドが抱える
class OnboardingWizard {
    private val roster = LinkedHashMap<String, Member>()   // 名簿
    private var seq = 0                                    // 発番用の連番
    private val profiles = LinkedHashMap<String, Profile>() // 部屋・道具の割り当て
    private val sentNotices = mutableListOf<String>()       // 送った触れ状の控え
    private val ledger = mutableListOf<String>()            // 本部へ計上した簿冊

    fun onboard(applicant: Applicant): Member {
        // すでに名簿にいるなら、二重に迎え入れない(触れ状も計上も増やさない)
        roster.values.firstOrNull { it.name == applicant.name }?.let { return it }

        // ① 登録:会員IDを発番して名簿に載せる
        seq += 1
        val id = "M%03d".format(seq)
        val member = Member(id, applicant.name, applicant.craft)
        roster[id] = member

        // ② 初期設定:部屋番号と初期道具を割り当てる
        val room = 100 + profiles.size + 1
        val tools = when (applicant.craft) {
            "鍛冶" -> listOf("金槌", "火箸")
            "織物" -> listOf("杼", "梳き櫛")
            else   -> listOf("汎用道具箱")
        }
        val profile = Profile(member.id, room, tools)
        profiles[member.id] = profile

        // ③ 通知:歓迎の触れ状を送る(控えを残す)
        sentNotices.add("${member.name}様、入会を歓迎します。部屋は${profile.room}号室、初期道具は${profile.tools.joinToString("・")}です。")

        // ④ 計測:入会統計を本部の簿冊へ計上する
        ledger.add("計上: ${member.id} / ${member.craft}")

        return member
    }

    // 読み出し(結果の確認用)
    fun members(): List<Member> = roster.values.toList()
    fun notices(): List<String> = sentNotices.toList()
    fun ledgerEntries(): List<String> = ledger.toList()
    fun profileOf(id: String): Profile? = profiles[id]
}

見慣れない書き方に、先に触れておく。LinkedHashMap は、入れた順序を覚えている連想配列(マップ)だ。名簿に登録した順を、そのまま保ってくれる。"M%03d".format(seq) は、番号を三桁の0埋めで文字にする書き方だ(1 なら M001)。when (applicant.craft) { … } は、値によって結果を選ぶ書き方で、ここでは職種に応じて初期道具の一覧を選び、そのまま tools に入れている。firstOrNull { it.name == ... } は、条件に最初に合う一人を返し、いなければ空(null)を返す——波かっこの中の it は、いま調べている一人ひとりを指す、仮の呼び名だ(ラムダに渡される値を、名前を省いて it と呼んでいる)。?.let { return it } は、その人がすでにいたら、そこで儀式を切り上げて帰る、という意味だ。

長らく、これで回っていた。だが組合が大きくなるにつれ、「触れ状の文面を今風にしたい」「新しい職種の初期道具を足したい」という直しの注文が増えてきた。そのたびに私は、この一本の呪文の、真ん中に手を入れることになる。

先日、肝を冷やした。歓迎の触れ状の文面を、一語だけ今風に直そうとして、呪文の真ん中の「通知のくだり」に手を入れたときだ。ところが、そのすぐ上に書いてあった「部屋割りのくだり」まで、一緒に手が触れてしまった。私は、新入りを一つ違う部屋へ案内してしまった。触れ状を直したかっただけなのに、隣に書いてあったというだけの部屋割りが、巻き込まれたのだ。困った顔で戻ってきた新入りに、私は平謝りした。その日のうちに気づけたから収まったが、指先が冷えた。応対の一つを直すのに、どうして、別の応対まで巻き添えにしなければならないのか。

それ以来、たとえ一語直すだけでも、私は入会の儀を頭から丸ごと唱え直して、登録も通知も計測も、ぜんぶ一緒に走るのを見届けないと、落ち着かなくなった。通知の文面を一度試したいだけでも、本物の名簿に登録が入り、本部の簿冊に計上が積まれてしまう。一つの働きだけを、独りで試せたら。いつも、そう思っていた。四つが一本に融け合っていて、一部分だけを、この呪文は、どうしても許してくれなかった。

窓口はそのまま、奥を分けたらしい

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

隣の組合の受付係が、こんな話をした。「うちの入会の儀を、丘のアカデミーの見習いに直してもらったんだ」。彼女が言うには、その見習いは、受付の使い勝手はそっくりそのままに——つまり窓口は前と同じなのに、奥の作りだけを、役目ごとにきれいに分けてくれたのだという。「窓口はそのまま、奥を分ける、っていうのがね。受付を預かる身には、妙に引っかかる直し方でね」。

私も、その言い回しに引っかかった。窓口はそのまま、奥を分ける。それに、と彼女は付け加えた。「あの見習いには、傍らでずっと黙って見ている、年寄りの師がいるらしい。マグナスさま、と呼ばれていた」。

丘のルーク、そして師のマグナス。私は、あの一語直して部屋割りを巻き込んだ日のことを思い返しながら、その二人の名を覚えて帰った。翌日、私は入会の儀の呪文の写しを抱えて、アカデミーの丘を上った。

工房は、宙に浮かぶ光の粒と、古い書物の匂いで満ちていた。奥の机で、若い見習いが手を動かしている。見ると、一枚の紙に、ごちゃごちゃに絡まった一本の長い線が描いてあった。その子は、色の違う筆を何本か手にして、その絡まった線を、まず役目ごとに——ここは赤、ここは青、と塗り分けている。塗り分けたあとで、色の変わり目のところで、別々の紙へ一本ずつ、線を写し取っていく。

「切ってから分けるんじゃない……役目で分けてから、切るんだ」。そう手元で独りごちながら。

その傍ら、卓の隅に、年寄りが一人座っていた。卓上に、小窓が一つだけ空いた小さな衝立を立てて、その奥に、木でできた小さな人形を数体、並べている。そして、窓の内側で、人形を一体ずつ、順に前へ送り出しては、また引っ込める。表の小窓からは、いつも一体しか見えない。それをまた繰り返している。私には、その手すさびが、退屈しのぎにしか見えなかった。何をしているのかは、気にも留めなかった。

私が声をかけると、見習いは筆を置いて顔を上げた。「あ……入会の儀の、ご相談ですか」。これがルークか。私は呪文の写しを卓に広げ、事情を語った。四つの働きが一本に詰まっていること。先月、通知を直そうとして部屋割りを巻き込んだ一件。今も、一語直すだけで儀式を丸ごと唱え直している綱渡りのこと。

ルークは写しをじっと見て、静かに言った。「これは……四つの違う働きが、一つの呪文に、上から順に、ぜんぶ詰め込まれていますね。だから、通知を直そうとすると、すぐ上の部屋割りに、手が触れてしまう」。

そのとおりだ、と私は頷いた。「一部分だけ、を、この呪文は許してくれないんだよ。ぜんぶが、一本に融けてる」。

こういう、一つのクラスが、変更する理由の違う働きをいくつも抱え込んで、一箇所の直しが広い範囲に響いてしまう魔導式を、神クラスと呼ぶのだそうだ。ひとつの魔法陣に、攻撃も回復も飛行も詰め込んだような、欲張りな陣。まさに、私の入会の儀のことだった。

手が、先に走る

ルークは、写しをもう一度見つめると、宣言もぼやきもせず、いきなり手を動かし始めた。呪文の並びを目で追いながら、紙にクラスを次々と描いていく。私には、その勢いが、少し前のめりに見えた。

呪文は、登録して、設定して、通知して、計測する、という手順で並んでいる。ルークは、その区切りごとに、クラスを一つずつ立てていった。登録するやつ、設定するやつ、通知するやつ、計測するやつ。そして、そのクラスの間に、「これが済んだら、これを呼んで」と、依存の矢印を描き足していく。登録の次に設定、設定の次に通知、通知の次に計測。一本の手順が、そのまま、クラスの鎖になっていく。

ルークが描いていたのは、こういう形だった。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 手順の一区切りごとに、クラスを立てる。
// ——神クラスの onboard() が処理を手順順に並べていたので、その順で区切ってクラスにした。
//    ところが、各クラスが「次に誰を呼ぶか」を、自分で抱えてしまう。
class NotifyStep(private val next: MeasureStep) {
    fun run(member: Member, profile: Profile) {
        /* 通知(触れ状)… */
        next.run(member)          // ← 自分で次(計測)を呼ぶ
    }
}
class ConfigStep(private val next: NotifyStep) {
    fun run(member: Member) {
        val profile: Profile = TODO() /* 初期設定… */
        next.run(member, profile) // ← 自分で次(通知)を呼ぶ
    }
}
class RegisterStep(private val next: ConfigStep) {
    fun run(applicant: Applicant) {
        val member: Member = TODO() /* 登録… */
        next.run(member)          // ← 自分で次(初期設定)を呼ぶ
    }
}
class MeasureStep {
    fun run(member: Member) { /* 計測(計上)… */ } // ← 鎖の終わり。ここだけ、次を呼ばない
}

これは、ルークが盛りかけて捨てた案なので、細部まで追わなくていい(TODO() は「まだ中身を書いていない」という印だ)。目で追ってほしいのは、next.run(...) という一手が、後ろのクラスを呼ぶために入っていることだ。登録のクラスは「次に設定を呼ぶ」ことを自分で知っていて、設定のクラスは「次に通知を呼ぶ」ことを自分で知っている。登録・設定・通知の三つは、それぞれ、自分の次を呼ぶ責任まで抱えている(末尾の計測だけは、鎖の終わりなので次がない)。こうして、四つのクラスが、一本の鎖につながっていく。

最後の、通知から計測へと向かう矢印を引いたところで、ルークの筆が止まった。私にも、紙の上で、矢印がひと続きの鎖に入り組んでいくのが見えた。組み立てるにも、後ろから順に相手を渡して、RegisterStep(ConfigStep(NotifyStep(MeasureStep()))) と、数珠つなぎにするしかない。ずいぶん、こみ入った形だ。

(また、ややこしくする気か。奥を片付けると聞いて、来たんだが)。私は思わず、眉をひそめた。ルークの手が止まったのを見て、私の中で、あの部屋割りを巻き込んだ日の、指先の冷たさが、ふっと蘇る。やっぱり、そう簡単に、切り分けられるものじゃないのか。声には出さなかった。ただ、期待した分だけ、肩に力が入った。

ルークの向こうで、隅の老人は、あいかわらず衝立の奥の人形を、一体ずつ順に送り出していた。私は、それが目に入っても、やはり気に留めなかった。

切る場所が、逆だった

ルークは、鎖だらけの紙から、筆を離した。そして、さっき役目を塗り分けていた、あの色筆を、もう一度手に取った。

縺れた矢印が走る紙の上に、今度は、線ではなく「役目」を、色で塗り直していく。登録の働きを一つの色、設定を別の色、通知を、計測を。すると、手順の矢印は色の下に沈み、代わりに、同じ色でまとまった島が、いくつか浮かび上がってきた。役目の島だ。そして、島と島の間には、一本の窓口だけが、ぽつんと残った。

「……切る場所が、逆だった」。ルークが、低く呟いた。「手順で切ってた。役目で、切るんだ」。

そして、自分の手元の島を見つめたまま、言葉を継いだ。「手順の区切りでクラスにしたから、それぞれが『次に誰を呼ぶか』を抱え込んで、鎖になった。通知を直したくても、通知のクラスが、計測を呼ぶ責任まで持っている。だから、独りにできない。……役目で分ければいい。登録は登録だけ、通知は通知だけ。互いに、呼ばせない。呼ぶ順番のほうは、窓口ひとつに、預ける」。

私は、その様子を、ただ見ていた。誰かに問われたわけでも、急かされたわけでもない。あの見習いは、自分で盛りかけて、自分で手を止めて、自分で引き返した。受付を預かってきた私には、途中で引き返すのが、どれだけ難しいかが、身にしみて分かる。

「分ける、が悪かったんじゃない」。ルークは、自分に言い聞かせるように言った。「どの向きで分けて、順番を誰に持たせるか、だ」。

このとき私は、まだ半信半疑だった。手順で切っても、役目で切っても、結局できあがるのは「登録・設定・通知・計測」の同じ四つの箱だろう。何がそんなに違うのか。——だが、あとで分かる。同じ四つでも、その四つが互いを呼び合って一本の鎖になるのか、それとも、順番を窓口ひとつに預けて、めいめいが独り立ちの島になるのか。違うのは、箱の顔ぶれではなく、箱と箱をつなぐ配線のほうだった。

ルークは手元の羊皮紙に、青と金の光で描かれた二つの魔法回路を並べてみせた。それは、彼が今しがた頭の中で組み立て直した、新しい「配線」の blueprint(設計図)だった。

手続き型の依存の鎖(却下案)と責任ベースのFacade(採用案)の魔法回路対比図

「ね?」とルークは二つの魔法回路を順に指さした。

「左側が、さっき僕が手順順に作りかけた『鎖』です。一見するとクラスに分かれていますが、自分の仕事を終えたクラスが、次のクラスを自分で呼んでいます。これだと、隣り合うクラス同士が直接結ばれてしまって、結局は一本の太い呪文だった頃と変わらない。通知だけを独りで試そうとしても、その先にある計測まで連動して走ってしまいます」

彼は次に、右側の魔法回路を指でなぞった。

「でも、右側の形なら、四つのクラスは互いのことを一切知りません。自分がやるべき『役目』だけを静かにこなして、次の人を呼ぶ手札も持っていない。その代わり、順番をコントロールする『窓口』をひとつだけ上に立てて、段取りはすべてそこに預けるんです。これなら、一本の鎖を完全に解きほぐせます」

役目で、分ける

ルークは、色分けした紙をそっと机に置くと、新しい魔導式に取りかかった。「四つの役目を、それぞれ独りで働ける専門家にします。そして、順番を取り次ぐ窓口を、一つだけ立てます」。

まず、四つの専門家から。

 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
41
42
43
44
45
46
47
48
// 責務① 登録:名簿への登載と会員ID発番だけを受け持つ
class MemberRegistry {
    private val roster = LinkedHashMap<String, Member>()
    private var seq = 0
    fun memberByName(name: String): Member? = roster.values.firstOrNull { it.name == name }
    fun register(applicant: Applicant): Member {
        seq += 1
        val member = Member("M%03d".format(seq), applicant.name, applicant.craft)
        roster[member.id] = member
        return member
    }
    fun members(): List<Member> = roster.values.toList()
}

// 責務② 初期設定:部屋番号と初期道具の割り当てだけを受け持つ
class ProfileInitializer {
    private val profiles = LinkedHashMap<String, Profile>()
    fun initialize(member: Member): Profile {
        val room = 100 + profiles.size + 1
        val tools = when (member.craft) {
            "鍛冶" -> listOf("金槌", "火箸")
            "織物" -> listOf("杼", "梳き櫛")
            else   -> listOf("汎用道具箱")
        }
        val profile = Profile(member.id, room, tools)
        profiles[member.id] = profile
        return profile
    }
    fun profileOf(id: String): Profile? = profiles[id]
}

// 責務③ 通知:歓迎の触れ状を送り、控えを残すことだけを受け持つ
class WelcomeNotifier {
    private val sentNotices = mutableListOf<String>()
    fun sendWelcome(member: Member, profile: Profile) {
        sentNotices.add("${member.name}様、入会を歓迎します。部屋は${profile.room}号室、初期道具は${profile.tools.joinToString("・")}です。")
    }
    fun notices(): List<String> = sentNotices.toList()
}

// 責務④ 計測:入会統計を本部の簿冊へ計上することだけを受け持つ
class SignupMetrics {
    private val ledger = mutableListOf<String>()
    fun record(member: Member) {
        ledger.add("計上: ${member.id} / ${member.craft}")
    }
    fun ledgerEntries(): List<String> = ledger.toList()
}

ここでルークが言った「役目」というのが、設計の言葉で言う責務だ。そのクラスが受け持つ役目のことで、言い換えれば、そのクラスを「直したくなる理由」のことでもある。関係する働きを一つにまとめ、関係ない働きは別に切り離しておく——これを、関心の分離という。

「登録は登録の名簿だけ、通知は触れ状の控えだけを持ちます」とルーク。「それぞれが、自分の役目に要るものだけを持っていて、他の役目のことは、何も知りません」。

私は、その「役目」というのが、まだ少しつかめなかった。「その役目というのは、どう見分けるんだ。登録も通知も、同じ入会の儀の一部だろう」。

「どういうときに直したくなるか、で見分けます」とルークは言った。「触れ状の文面を今風にしたい——これは、通知を直したい理由です。新しい職種の初期道具を足したい——これは、部屋割りを直したい理由。直したくなる理由が別なら、それは別の役目なんです。理由の違うものを一つに混ぜて書いていたから、通知を直そうとして、隣の部屋割りまで巻き込まれた」。

私は、あの一語直して部屋割りが巻き込まれた日を、思い出していた。「……直す理由が違うものを、別々にしておく、ということか」。

「そうです」とルーク。「では、通知の文面だけ直したいとき、登録まで走らせずに、済むのか」——私が知りたかったのは、そこだった。ルークは頷いた。「済みます。通知のクラスは、他の誰も呼びません。通知だけを、独りで試せます」。

関係ないところが、巻き込まれない。私は、ようやく胸のつかえが少し降りるのを感じた。

順番は、窓口ひとつに

「そして、この四人の専門家を束ねる、窓口を立てます」。ルークは、島と島の間に残った、あの一本の窓口を、魔導式にした。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 窓口:四つの専門家を外から受け取って保持し、
// 「入会の儀の段取り(呼ぶ順序)」だけを、一箇所に集める。
// 各専門家は互いを一切知らない。順序を知っているのは、この窓口だけ。
class OnboardingService(
    private val registry: MemberRegistry,
    private val initializer: ProfileInitializer,
    private val notifier: WelcomeNotifier,
    private val metrics: SignupMetrics,
) {
    fun onboard(applicant: Applicant): Member {
        // 段取り: すでに名簿にいるなら、二重に迎え入れない
        registry.memberByName(applicant.name)?.let { return it }
        // 段取り: 登録 → 初期設定 → 通知 → 計測 の順に、各専門家へ委ねるだけ
        val member = registry.register(applicant)
        val profile = initializer.initialize(member)
        notifier.sendWelcome(member, profile)
        metrics.record(member)
        return member
    }
}

こうして、複雑になった奥を、一つの窓口の裏に隠し、使う側には簡潔な入口だけを見せる定石を、Facade(ファサード)と呼ぶのだそうだ。建物の正面を意味する言葉らしい。使う側は、奥に四人の専門家がいることを知らなくていい。ただ onboard という窓口の一手を叩けば、あとは窓口が、登録・設定・通知・計測を、順番に取り次いでくれる。

見慣れない書き方に、触れておく。窓口の頭にある class OnboardingService(private val registry: MemberRegistry, …) は、使う四人の専門家を、自分の中で勝手に作るのではなく、外から渡してもらって、自分の中に持っておく、という書き方だ。こうしておくと、あとで試すときに、たとえば通知役だけを独りで取り出して確かめる、といったことがしやすくなる。

私は、ここで一つ、引っかかった。「さっき君が鎖にしかけた書き方も、クラスに分けてはいたよな。あれと、今のこれは、どう違うんだ。どっちも、分けているだろう」。

ルークは、捨てた案の紙を、もう一度こちらに向けた。「さっきの通知役は、こう書いていました」。

1
2
3
4
5
6
7
// 捨てた案:通知役が、自分で次の計測役を呼んでいた
class NotifyStep(private val next: MeasureStep) {
    fun run(member: Member, profile: Profile) {
        /* 通知(触れ状)… */
        next.run(member)          // ← 自分で次(計測)を呼ぶ
    }
}

next.run で、自分で次の計測役を呼んでいます。次を呼ぶ責任を、通知役が、自分で抱えていた」。そして、採用した形を指す。

1
2
3
4
5
6
// 採用した形:通知役は、次を呼ばない。呼ばれもしない。
class WelcomeNotifier {
    private val sentNotices = mutableListOf<String>()
    fun sendWelcome(member: Member, profile: Profile) { /* 触れ状を送り、控えを残すだけ */ }
    // …次を呼ぶ一手が、どこにも無い
}

「今度の通知役は、次を呼びません。呼ばれもしません。順番は、窓口の onboard だけが持っています」。ルークは、二つの紙を並べた。「分けるかどうか、じゃなかったんです。どの向きで分けて、順番を誰に持たせるか、でした。手順で切って、互いに呼ばせれば、鎖になる。役目で切って、順番を窓口ひとつに預ければ、はじめて、解ける」。

各クラスが、次に誰を呼ぶかを知っているか、いないか。似た四つのクラスの、たった一つの違いは、そこにあった。通知役が次を呼ばないから、通知役は、独りで立てられる。独りで、試せる。

私は、念のため確かめた。「では、この窓口さえ立てれば、片付くのか」。

「いえ、逆です」とルークは、はっきり言った。「先に、役目で分けたから、窓口が『順番を取り次ぐだけ』の、薄いもので済むんです。もし、分けずに、この窓口の中へ、四つの働きの中身を全部書き込んだら——窓口が、名前を替えただけの、元の神クラスに戻ります。分けるのが先で、窓口は、後なんです」。

(ちなみに、Kotlinには、別の部品へ処理を丸ごと横流しする委譲の書き方もある。だが窓口の値打ちは、四つの段取りを組み立てることにあって、ただ横流しするのとは違う。だから onboard は、普通のメソッドとして書く。)

仮の受付で、独りずつ試す

「本物の入会名簿に手を入れる前に」とルークは言った。「結界の中に、仮の受付を立てて、登録・設定・通知・計測が、それぞれ別々に、独りでちゃんと働けるかを確かめます」。

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

1
2
3
4
5
val applicants = listOf(
    Applicant("アイナ", "鍛冶"), // → M001・部屋101・[金槌, 火箸]
    Applicant("ベル", "織物"),   // → M002・部屋102・[杼, 梳き櫛]
    Applicant("カイ", "石工"),   // → M003・部屋103・[汎用道具箱](既定)
)

会員の札は登録順に M001M002M003。部屋は、割り当てた順に101号室から。道具は職種ごとに決まっていて、石工のように表に無い職種には、既定の「汎用道具箱」が渡る。

まず、窓口経由で三人を迎えると、名簿も部屋も触れ状も計上も、狙いどおりに揃うこと。次のコードに出てくる assertEquals は、狙いどおりの形と、実際にそうなった形とを、照らし合わせる詠唱だ。profileOf(id)!!!! は、「ここは必ず値がある」とみなして中身を取り出す書き方で、もし空(null)ならその場で止まる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 1. 窓口経由で三人を迎え、四つの働きがすべて狙いどおりになる
val registry = MemberRegistry()
val initializer = ProfileInitializer()
val notifier = WelcomeNotifier()
val metrics = SignupMetrics()
val service = OnboardingService(registry, initializer, notifier, metrics)

applicants.forEach { service.onboard(it) }

assertEquals(listOf("M001", "M002", "M003"), registry.members().map { it.id })
assertEquals(listOf(101, 102, 103), registry.members().map { initializer.profileOf(it.id)!!.room })
assertEquals(
    listOf(
        "アイナ様、入会を歓迎します。部屋は101号室、初期道具は金槌・火箸です。",
        "ベル様、入会を歓迎します。部屋は102号室、初期道具は杼・梳き櫛です。",
        "カイ様、入会を歓迎します。部屋は103号室、初期道具は汎用道具箱です。",
    ),
    notifier.notices(),
)
assertEquals(listOf("計上: M001 / 鍛冶", "計上: M002 / 織物", "計上: M003 / 石工"), metrics.ledgerEntries())

次に、私がいちばん確かめたかったこと——通知役を、たった独りで働かせられること。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 2. 通知役を独りで立て、他の三人を一切用意せずに、触れ状だけを試す
val notifier = WelcomeNotifier()
notifier.sendWelcome(
    Member("M001", "アイナ", "鍛冶"),
    Profile("M001", 101, listOf("金槌", "火箸")),
)
assertEquals(
    listOf("アイナ様、入会を歓迎します。部屋は101号室、初期道具は金槌・火箸です。"),
    notifier.notices(),
)

登録役も、設定役も、計測役も、一つも用意していない。それでも、通知役は独りで、ちゃんと触れ状の控えを残した。名簿を汚さず、本部の簿冊に計上を積むこともなく、通知だけを試せる。あの、一部分だけを許してくれなかった呪文が、嘘のようだった。

三つ目は、私が本当に念を押したかったところだ。奥を分けても、書き換える前の一本の呪文と、寸分同じ入会の儀になること。

1
2
3
4
5
6
7
// 3. 書き換える前(神クラス)と、寸分同じ結果になる
val before = OnboardingWizard()
applicants.forEach { before.onboard(it) }

assertEquals(before.notices(), notifier.notices())          // 触れ状が寸分一致
assertEquals(before.ledgerEntries(), metrics.ledgerEntries()) // 計上簿が寸分一致
assertEquals(before.members().map { it.id }, registry.members().map { it.id }) // 名簿が寸分一致

「これが、いちばん大事なところです」とルーク。「奥を役目で分けても、書き換える前の呪文と、まったく同じ結果になる。受付のやり方も、出来上がる名簿も、触れ状も、一文字も変わっていない——それを、機械の目で照らして確かめます」。

前と寸分同じ。私が心配していた一点に、この三行がそのまま応えていた。設計を整えただけで、中身は変えていない。だから、事故にはならない。最後に、同じ人が二度来ても、二重に迎え入れないこと。

1
2
3
4
5
6
7
8
// 4. 同じ申込者を二度迎えても、名簿1件・触れ状1通・計上1件のまま(二重に迎え入れない)
val dup = Applicant("アイナ", "鍛冶")
service.onboard(dup)
service.onboard(dup)

assertEquals(1, registry.members().size)
assertEquals(1, notifier.notices().size)
assertEquals(1, metrics.ledgerEntries().size)

すでに名簿にいる人なら、窓口はそこで儀式を切り上げる。触れ状が二重に飛ぶことも、計上が二重に積まれることもない。これは、書き換える前の呪文と、まったく同じふるまいだ。結界の中の仮の受付は、すべて、狙いどおりに通った。

通知だけ、独りで触れる

役目ごとに分けられ、一つの窓口で束ねられた入会の儀を、私は受け取った。窓口は一つ。その奥で、登録・設定・通知・計測が、それぞれ独立した専門家になっている。

「これなら、触れ状の文面を直したいとき、通知役だけを開けばいいんだな」と私は言った。「登録も、計測も、走らせずに」。

「はい」とルーク。「窓口の中の順番は変わりませんから、受付のやり方は、前とそのままです。奥の一人だけを、独りで直せます」。

私は、念のため確かめた。「では、この窓口さえあれば、この先どんな直しも、安全なのか」。

ルークは、正直に答えた。「いえ。役目で分けたから、一つを直しても、隣に手が届きにくくはなりました。でも、窓口の中の『順番』そのものを書き替えれば、儀式の流れは変わります。何でも安全になる魔法ではなく、どこを触れば、何に響くのかが、はっきり見えるようになった、ということです」。

「それで、十分だよ」と私は言った。「どこに響くのか分からないのが、いちばん怖かったんだ」。

私は、「試しに、私の手でやってみていいか」と切り出した。まず、通知役だけを独りで呼んで、前から気になっていた触れ状の文面を、一語だけ変えて、控えを確かめる。登録も、計測も、走らない。指先が、拍子抜けするほど軽かった。次に、窓口経由で新人を一人迎えて、名簿と部屋と触れ状と計上が、ちゃんと揃うのを見る。一部分だけを触る、というのが、こんなに当たり前にできるとは。あの日の指先の冷たさは、もう無かった。

私が帰り支度をしていると、それまで衝立の奥で人形を送り出すばかりだった隅の老人が、その手を、ふいに止めた。そして、こちらへ——ルークにではなく、私に——ゆっくりと視線を向けた。何か確かめたいことがある、という、穏やかな目だった。

「あの子の引いた線、あなたにはどう映ったね」。

詰めるでも、試すでもない、静かな問いだった。私は面食らいながらも、受付柄、思ったままを返した。「……分ける前より、どこに何があるか、はっきり見えました。窓口は一つのまま、奥が、役目でちゃんと片付いていて」。

老人は、小さくうなずいただけで、また人形へ手を戻した。その所作を見て、私はようやく腑に落ちた。ああ、この方が、あの子のお師匠さまか。ルークが去り際に「マグナスさま、失礼します」と会釈したのが、それを確かめさせた。あの黙って見ていた人は、この日、口を開いたのは、その一問きりだった。

組合へ戻った私は、さっそく、通知役だけを独りで開いて、前から気になっていた触れ状の文面を、今度こそ直してみた。登録も、計測も、走らない。直して、試して、また直して。関係ないところが、もう、巻き込まれない。一つずつ、独りで触れる。ただそれだけのことが、私の受付を、こんなに軽くするのか。窓口はひとつのまま、私は今日、奥の一人と、ようやく落ち着いて向き合えた気がした。


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

  • 修復した呪文(クラス/モジュール名): MemberRegistry(登録)/ProfileInitializer(初期設定)/WelcomeNotifier(通知)/SignupMetrics(計測)=役目ごとに分けた四人の専門家。OnboardingService(窓口=Facade。呼ぶ順番の取り次ぎだけを持つ)
  • 乱れの要因(アンチパターンの課題):
    • 入会の儀の OnboardingWizard が、登録・初期設定・通知・計測という、変更する理由の違う四つの働きを、一つのメソッドに手順順で抱えていた(神クラス)。現状は正しく動くが、通知を一語直すだけでも、すぐ上の部屋割りに手が触れて壊す危険があり(実際に一度、部屋割りがずれた)、通知だけを独りで試すこともできなかった。
  • 過剰設計の反省(背伸びの過ち):
    • 神クラスの手順の並びを見て、その区切りごとにクラスへ分け、各クラスが次のクラスを自分で呼ぶ鎖(next.run の数珠つなぎ)にしかけた。だが、段取り(呼ぶ順番)の知識が各クラスに散り、一つを直すと前後まで巻き込む——神クラスを「小さな神クラスの鎖」に切り分けただけだと気づき、手を止めた。分けるのが悪いのではなく、手順で切って互いに呼ばせたのが、逆だった。
  • 魔術の定石(学んだ設計パターン):
    • 責務分割 + Facade。変更する理由(役目=責務)ごとに専門クラスへ分け、各クラスは互いを呼ばず、自分の役目だけを持つ。複雑になった奥を、OnboardingService という一つの窓口(Facade)で隠し、窓口だけが呼ぶ順番を持つ。使う側は、窓口だけを知ればよい。手順で切って互いに呼ばせると鎖になり、役目で切って順番を窓口ひとつに預けると解ける——違いは、どの軸で分け、段取りを誰に持たせるか、だった。
  • 模擬詠唱の記録(検証のポイント):
    • 窓口経由で入会の儀が通ること、通知役などの専門家が単独でも独りで働けること(=一部分だけを試せる)、書き換える前の一本の呪文と寸分同じ結果になること(=設計を変えても中身は変わらない)、同じ人を二度迎えても二重に迎え入れないことを確かめた。
  • ちなみに:
    • 複雑な奥を一つの窓口で隠す定石を Facade と呼ぶ。ただし窓口は「取り次ぎ(段取りの合成)」に徹すること。奥を役目で分けずに、窓口へ中身を全部書き込むと、窓口が「ラベルを替えただけの神クラス」になる(Fat Facade)。だから、分けるのが先、窓口は後。似た定石に、相手を共通の形へ変える Adapter、同格の面々の相互通信を集める Mediator があるが、いずれも狙いが違う。
  • 師匠マグナスの格言:
    • 「あの子の引いた線、あなたにはどう映ったね」(この日、マグナスさまは終始無言で、衝立の奥の木偶人形を順に送り出すばかりだった。口を開いたのは、依頼人セイラへの、この一問だけ。師の答えではなく、依頼人が持ち帰った言葉を、格言の代わりに記しておく——「分ける前より、どこに何があるか、はっきり見えました。窓口は一つのまま、奥が、役目でちゃんと片付いていて」)
comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。