Featured image of post コードウィザード【Factory Method】報せの作り方が窓口ごとに散らばり、新しい種類を足すたびに全部の窓口を直す羽目になる〜生成を一つの呪文に集め、種を増やすのは一箇所だけにする〜

コードウィザード【Factory Method】報せの作り方が窓口ごとに散らばり、新しい種類を足すたびに全部の窓口を直す羽目になる〜生成を一つの呪文に集め、種を増やすのは一箇所だけにする〜

ギルドの報せ係リタは、通知の届け方を増やすたび、締切や招集など全部の窓口に同じ振り分けを書き足していた。一箇所書き忘れ、急ぎの招集が届かない。見習い魔術師ルークは「究極の報せ生成陣」を盛って自滅し、師匠の「一番小さな道具で足りないか」から、生成を一箇所に集める Factory Method へ。Kotlin で描く。

書き忘れた一通

わたしはリタ。冒険者ギルドで、報せ係をしている。

ギルドは、冒険者にいろんな報せを送る。依頼の締切、急ぎの招集、討伐が済んだという完了の報せ。届け方も、いくつかある。文鳥便——文鳥に文(ふみ)を持たせて運ぶ、詳しいけれど少し遅い便り。早鐘の短信——鐘を鳴らして報せる、短くて速いやつ。それに、最近入れたばかりの光触れ。空に光の印をぱっと灯して、その場の全員に届く、いちばん急ぎの報せ。報せは、要る人に、要る速さで届けてこそ。わたしは、それだけを守ってきた。

ただ、ずっと困っていることがあった。届け方が一つ増えるたびに、わたしは、報せを送る窓口の全部に、同じ書き足しをして回らなきゃいけない。締切の窓口、招集の窓口、完了の窓口……どこも、「この合言葉なら、この届け方」という振り分けを、それぞれが抱えている。種類が増えれば、その全部に、同じ一行を書き写す。台帳を何冊も、同じ手で書き換えるみたいに。

そして、先日。新しい光触れを入れたとき、わたしは、やってしまった。

締切の窓口には、ちゃんと光触れを足した。なのに——招集の窓口に、足し忘れた。

その日、急ぎの討伐の招集を、光触れで出そうとした。なのに招集の窓口は、光触れなんて知らない、という顔で、わたしの報せを撥ねた。冒険者に急報は届かず、討伐隊は、定刻に集まらなかった。

わたしが、たった一箇所、書き忘れただけで。

きっちりやってきたつもりだった。でも、窓口の数だけ同じことを書いて回る限り、どこか一つ抜ければ、こうなる。報せの種類は、これからも増える。このままじゃ、また誰かに、急報が届かない。

丘の見習いの名

心当たりは、あった。

報せ係をしていると、いろんな礼状や口コミが、わたしのデスクを通る。それで近頃、ルークという見習いの名を、何度も見かけていた。雑貨屋の会計を直した、蔵書庫の検索台を直した、町の地図を直した——その礼が、みんな、ここを通っていったのだ。だから丘の上のアカデミーへ上るのに、迷いはなかった。ああ、噂のあの子のところへ行けばいい、と。

工房は、古い書物の匂いで満ちていて、見上げれば、壁という壁が書架で埋まっていた。報せ係のわたしの目は、それより先に、隅で山積みになった文机のほうへ吸い寄せられた。

その奥で、見習いらしい若者が一人、わたしの用も聞かないうちから、机いっぱいに広げた大掛かりな魔法陣に、夢中で手を入れていた。何かを組み上げている最中らしい。わたしが声をかけると、「あ、いいところに!」と顔を上げる。これが、噂のルークか。思っていたより、ずいぶん若い。それに——ずいぶん、忙しそうだ。

わたしは事情を話して、散らばった振り分けの符を見せた。ルークはそれを手元に引き寄せ、まず報せにまつわる「もの」から、順に説いてくれた。

1
2
3
4
5
interface Notification {
    fun send(message: String): String
}
class MailFamiliar : Notification { override fun send(message: String) = "文: $message" }
class ShortBell   : Notification { override fun send(message: String) = "早鐘: $message" }

Notification というのが、報せの「形だけの約束」——send と頼めば報せを送る、ということだけを決めた取り決めだ。これを インターフェース(中身ではなく「こう呼べばこう応える」という形だけを取り決めた設計図)と呼ぶのだそうだ。文鳥便も早鐘も、その約束を守って、それぞれの送り方をしている。「ここは、いいんです」とルーク。「問題は、こっちでした」。

彼が指したのは、窓口のほうだった。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 締切の報せを送る窓口
fun noticeDeadline(type: String, message: String) = when (type) {
    "mail" -> MailFamiliar().send(message)
    "bell" -> ShortBell().send(message)
    else   -> throw IllegalArgumentException("不明な報せ: $type")
}

// 招集の報せを送る窓口(同じ振り分けが、そっくり重複している)
fun noticeSummon(type: String, message: String) = when (type) {
    "mail" -> MailFamiliar().send(message)
    "bell" -> ShortBell().send(message)
    else   -> throw IllegalArgumentException("不明な報せ: $type")
}

when というのは、合言葉(type)によって行き先を振り分ける仕掛けだ。"mail" なら文鳥便、"bell" なら早鐘、という具合に。「締切の窓口にも、招集の窓口にも、まったく同じ振り分けが、そっくり書いてありますね」とルークが言う。そう、それだ。「届け方を一つ増やすたびに、わたし、全部の窓口に、同じ一行を書き足すの」。

ルークは小さく頷いた。「一つ直すために、あちこちの同じ箇所を、全部直して回らなきゃいけない。この状態には、名前があるんです」。

散弾銃手術——一箇所を変えるために、散らばった複数の箇所を、まとめて撃ち直す羽目になる状態のことを、魔術師はそう呼ぶのだという。光触れを足したとき、締切には書いたのに招集に書き忘れた。だから招集だけ、光触れを知らなかった。わたしのあの書き忘れには、ちゃんと名前がついていたのか。

究極の報せ生成陣

ところが、だ。

ルークの手が、うずうずと動いた。さっきまで夢中で手を入れていた、あの大掛かりな魔法陣のことが、頭から離れていなかったらしい。

「リタさん、ちょうどいい。これを機に——どんな報せにも、未来のどんな届け方にも応える、究極の報せ生成陣を、組み上げます!」

言うなり、彼は宙に、階また階を積み上げ始めた。報せを作る工房。その工房を作る、もう一段上の工房。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
abstract class NotificationFactory {
    abstract fun create(): Notification
}
class MailFactory : NotificationFactory() {   // 種類ごとに、専用の工房が要る
    override fun create() = MailFamiliar()
}
// BellFactory, LightFactory … と、種類の数だけ工房が増えていく

abstract class FactoryProvider {              // ← 工房を作る、上の工房
    abstract fun provide(type: String): NotificationFactory
}
class GuildFactoryProvider : FactoryProvider() {
    override fun provide(type: String): NotificationFactory = when (type) {
        "mail" -> MailFactory()
        // bell, light … ここの枝も増える
        else   -> throw IllegalArgumentException("不明: $type")
    }
}

abstract と付いたのは、それ自体では中身を持たない骨組みなのだと、後でルークが教えてくれた。報せを作る工房があり、その工房を選んで作る、もう一段上の工房がある。それだけでは終わらなかった。「報せに宛先や本文を、一画ずつ設える組立台も要りますね。それと、よく似た報せを雛形から写し取る、写し台も」。階の上に、また階。

わたしの目には、もう、何がどの階なのか、まるで追えなかった。「待って、ルーク。わたしが頼んだのは、報せを書き忘れずに届けたい、それだけなの。なんで……こんな、お城みたいになるの?」

一通送るだけで、ひと仕事

ルークは陣を起動して、試しに一通、文鳥便を送ろうとした。

ところが、その一通が、なかなか出ない。工房を選ぶ上の工房を呼んで、選ばれた工房に報せを作らせて、それからやっと送る。一通送るのに、階段を上って下りるみたいな大仕事だ。

しかも、だ。わたしは、いちばん肝心なことを尋ねた。「ねえ、それで。新しい届け方を足すときは、どうなるの?」

ルークは、説明しようとして、口ごもった。「ええと……新しい届け方ごとに、専用の工房クラスを一つ作って……それを、上の工房の振り分けにも足して……あと、報せのクラスも……」。

わたしは、思わず指を折って数えた。前は、窓口に一行ずつ書き足していた。それでも書き忘れた。なのに今度は、新しい届け方を一つ増やすのに、「報せのクラス」と「その専用の工房」と「上の工房の振り分け」と、前より多くの場所に、手を入れなきゃいけない。減らしたかったはずの手間が、増えている。お城を建てたのに、引っ越しがもっと大変になったみたいだ。

そのときだった。工房の奥の文机で、紙の擦れる、かすかな音がした。見れば、白髪頭の男がひとり、山と積まれた古い手紙を、一通ずつ静かに選り分けている。いつからそこにいたのだろう。顔も上げないまま、その人は、低くこう漏らした。

「一通運ぶのに……鳥は、一羽で、よかろうに」

ルークは、自分の建てた塔の中で迷子になっていて、聞こえていない。わたしのほうも、出入りの文使いが何か言ったのだろうと思い込んで、聞き流してしまった。

「明日もまた、招集を出すのよ」と、わたしは急かした。「これで、間に合うの?」。ルークは、自分が積み上げた階のどこで何が起きるのか、もう自分でも説明できなくなっていた。最初にわたしが持ち込んだ符より、ずっと大層なものになっている。とうとう、頭を抱えてしまった。

文を仕分ける老人

奥の老人が、手紙の束を、とん、と揃えて置いた。

そして、塔に埋もれたルークのほうへ、静かに声をかけた。その声に、ルークの背すじが、びくりと伸びた。積み上げた塔から慌てて顔を出すと、「……お、お師匠さま」と、消え入りそうな声で応える。わたしは、目をしばたたいた。文机で手紙を選り分けていた、あの人。出入りの文使いか何かと思い込んでいたその人こそ——この子の師匠、大魔術師マグナス様、その人だったのだ。

マグナス様は、積み上がった塔には、目もくれなかった。ただ、手の中の一通の手紙を、軽くかざして、問うた。

「お前さんは、立派な塔を建てた。だが——この報せを一つ、相手に届けるのに、その塔の、どれだけが要るのかね?」

ルークは、答えられない。

「“一番小さな道具で足りないか” と問うのが、いつでも先だよ」

マグナス様は、それだけ言うと、手の中の手紙へ目を落とし、また一通、選り分け始めた。

ルークは、しばらく自分の塔を見上げていた。それから、組み上げかけた巨大な生成陣から、ふっと手を離す。代わりに、机の隅に転がっていた、なんの飾りもない、一本の素な符を、つまみ上げた。

「一通の報せに、こんな大層な陣は……要らなかった。作る役目を、ひとつの場所に、ひとつだけ。それで、よかったんだ」

ひとつの場所に、ひとつだけ

ルークは、宙に積み上げた塔を、下の階から順に、ことごとく畳んでいった。階も、工房を作る工房も、跡形もない。代わりに、机の上に、一本の呪文を書いた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class LightSign : Notification {      // 光触れ(新しい届け方)
    override fun send(message: String) = "光触れ: $message"
}

// 「どの届け方を作るか」の役目を、この一箇所に集める
fun createNotification(type: String): Notification = when (type) {
    "mail"  -> MailFamiliar()
    "bell"  -> ShortBell()
    "light" -> LightSign()   // 新しい届け方を足すのは、ここ一行だけ
    else    -> throw IllegalArgumentException("不明な報せ: $type")
}

「どの届け方を作るか、を決める役目を、createNotification という一箇所の呪文に、ぜんぶ集めました」とルーク。「『ものを作る役目』を一箇所に閉じ込めて、種類を増やすときも、そこだけ直せばよくする。この定石を、そう呼ぶそうです」。

Factory Method——ものを作る役目を専用の窓口(呪文)に集約し、種類の追加を、その一箇所だけで済ませられるようにする設計の型なのだという。そして、窓口のほうは、こうなった。

1
2
fun noticeDeadline(type: String, message: String) = createNotification(type).send(message)
fun noticeSummon(type: String, message: String)   = createNotification(type).send(message)

締切の窓口も、招集の窓口も、もう振り分けを持っていない。ただ、createNotification に「これ作って」と頼んで、出てきた報せを送るだけ。あの、そっくり重複していた when が、両方から、きれいに消えている。

ルークは、自分の頭の中のイメージを整理するように、指先で小さく魔法陣を描いた。 「言葉だけで関係性を追うと、何がどこに依存しているか、少し見えづらいですよね。ちょっと魔力投影で、今回の構造を書き出してみます」

彼の指先から放たれた薄紫色の魔力光が、空中にスクロール状の設計図を広げた。そこに金と青のラインで描かれたのは、すっきりと整理された魔法式(クラス関係図)だった。

Class diagram showing Client requesting notification creation to Factory, using Notification interface implemented by MailFamiliar, ShortBell, and LightSign

「わあ……相変わらずルークの魔力投影は綺麗ね」と、わたしは目を輝かせた。「ええと、一番上が窓口(Client)で、左が生成(Factory)ね」

「はい!」と、ルークは嬉しそうに頷いた。「ポイントは、窓口が具体的な届け方(MailFamiliarLightSign)を一切見ていない点です。窓口は『作る呪文(createNotification)』を呼び出し、返ってきた『報せの境界(Notification インタフェース)』に向けて送信の合図を送るだけ。具体的な具象クラスは、すべて右下の境界線の向こう側に隠されています」

「なるほど……。窓口と届け方の間に、一枚の遮蔽板があるようなものね」

「その通りです! この遮蔽板(インタフェース)と、生成を一手に引き受ける窓口(Factory)のおかげで、依存のねじれが解消されているんです」

「じゃあ」と、わたしは身を乗り出した。「新しい届け方を足すときは?」

「この一箇所に、一行です。窓口には、もう、触りません」。

わたしは、ゆっくりと呑み込んだ。「……それって。あの、わたしの書き忘れが、起きる場所が——無くなる、ってこと?」

「はい」とルークは頷いた。「振り分けが一箇所だけなら、書き忘れようがありません。締切にも招集にも完了にも、同じ振り分けを書き写す必要が、もうないんです」。

ルークは、少し決まり悪そうに、付け加えた。

「ついでに、白状します。さっき僕は、報せに宛先や本文を一画ずつ設える『組立台』や、雛形を写す『写し台』まで、組もうとしていました。でも——もし報せに宛先や本文を持たせたくなっても、Kotlin なら、器を data class にして、作るときに名前を添えれば、一息に書けます。専用の組立台なんて、要らない。写しのほうも、その data class に備わった copy で済む。言語がもう肩代わりしてくれている道具を、僕は、わざわざ自分で組もうとしていたんです」。

大仰な台が、彼のひと言で、すっと消えた。

「もう一つだけ、いいですか」とルーク。「今は、届け方を "light" みたいな文字の合言葉で渡しています。これを、文字じゃなく型で表すと、さらに堅くなるんです」。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 届け方を、文字でなく「型」で表す別案
sealed interface Channel {
    object Mail  : Channel
    object Bell  : Channel
    object Light : Channel
}

fun createNotification(channel: Channel): Notification = when (channel) {
    Channel.Mail  -> MailFamiliar()
    Channel.Bell  -> ShortBell()
    Channel.Light -> LightSign()
    // else(その他)の枝は要らない
}

頭の sealed interface は、「取りうる届け方は、この一族だけ」と、型で出し切って封じる宣言なのだそうだ。文鳥便・早鐘・光触れ。この三つきり、と決めてしまう。それぞれの届け方は object——たった一つきりの実体で、名前のついた目印のようなもの——で表すから、MailFamiliar() のように作るのではなく、Channel.Mail とそのまま指す。文字版の窓口では、どんな文字が紛れ込むか分からないぶん、「その他」を受け止める else が要った。けれど sealed で届け方の一族を出し切ってしまうと、取りうる種類をコンパイラが残らず把握できる。だから三つの枝をすべて書けば「その他」はもう存在せず、else は要らない——裏を返せば、どれか一つでも書き忘れれば、その場で「足りない」と見抜かれる。これは、合言葉の文字を最初から型に置き換えた別の設計で、さっきの文字版の窓口にあとから接ぐ話ではない。窓口が "light" という文字を持ち回るか、Channel.Light という型を持ち回るか——その違いだけだ。

「こうすると」とルーク。「もし新しい届け方を一族に足して、この呪文に枝を書き足すのを忘れたら——魔法を編む段で、『枝が一つ足りない』と、はじかれます。送ってみるより前に、組み上がるより前に、気づける」。

「じゃあ……もう、誰も、忘れられないのね」。わたしは、思わず呟いた。あの日の、たった一箇所の書き忘れ。あれを、そもそも起こせなくしてしまう。

「散らばりを直したのは、さっきの集約のほうです」と、ルークは念を押した。「こっちは、その上に重ねる、念のためのもうひと押し。文字のままでも、集約だけで、散弾銃手術はちゃんと直っています」。

結界で、試しの報せを

「ギルドに本物の報せを流す前に、結界の中で、宛先のない試しの報せを何通か出して、狙いどおり届くか検めます」。

ルークは、そう言って模擬詠唱に取りかかった。わたしの目の前で、試しの報せを一通ずつ送ってみせ、出てきた答えを、その都度わたしに見せてくれる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertFailsWith

fun main() {
    assertEquals("文: 集合",     createNotification("mail").send("集合"))
    assertEquals("早鐘: 集合",   createNotification("bell").send("集合"))
    assertEquals("光触れ: 集合", createNotification("light").send("集合"))

    assertIs<LightSign>(createNotification("light"))

    assertFailsWith<IllegalArgumentException> { createNotification("fax") }

    // 招集の窓口は一切変えていないのに、光触れで出る(Before では撥ねていた)
    assertEquals("光触れ: 集合", noticeSummon("light", "集合"))

    println("ALL TESTS PASSED")
}

assertEquals は「出てきた報せが、刷り見本どおりか」を照らし合わせる道具。assertIs は「出てきたのが、狙った種類の報せか」を確かめる道具。assertFailsWith は「無い届け方を頼んだら、ちゃんと撥ねるか」を検める道具だという。

一つずつ、わたしの目の前で答えが出た。文鳥便、早鐘、光触れ。どれも刷り見本どおり。fax なんていう無い届け方は、ちゃんと撥ねられる。そして——最後の一つ。光触れを足すのに、招集の窓口(noticeSummon)には、わたしは一行も書き足していない。なのに、光触れの招集が、ちゃんと出た。あの日、撥ねられたあの一通が、今度は、出る。

結界の中に、「ALL TESTS PASSED」——みな、狙いどおり——と灯った。

一箇所だけ、足せばいい

整え直された報せ場を、わたしは受け取った。お城みたいな塔も、積み上がった階も、跡形もない。机の上にあるのは、生成を集めた一本の呪文と、驚くほど短い窓口だけ。

これなら……届け方を増やしても、もう、窓口を全部書いて回らなくていい。一箇所だけ。たった、一箇所。窓口の数だけ同じ振り分けを書き写して、どこか抜けていないかと毎晩見直していた、あの日々が、嘘みたいだ。肩の荷が、すっと下りた。

わたしも、自分の手で、もう一つ新しい届け方を足してみることにした——狼煙(のろし)。集約の呪文に一行、報せのクラスを一つ。それだけで、締切も、招集も、完了も、窓口には一切触れないまま、狼煙で出せるようになった。あの書き忘れは、もう、起こりようがない。

奥のマグナス様が、選り分けの手の合間に、こう寄こした。

「道具は、大きいから偉いのではない。手の中で、過不足ないのが、いちばん偉いのだよ」

わたしは礼を言って、丘を下りた。明日の招集は、もう、誰の手からもこぼれ落ちない。届けるべき報せが、届けるべき人のもとへ——あの日撥ねられた一通を思えば、それだけで、胸の奥が、静かに凪いでいた。


〔エピローグ〕見習いルークの覚え書き

その夜、工房の灯を細めて、僕は魔導書を開いた。

この季節、訪ねてくれた人たちのことを、書き留めておこうと思ったのだ。雑貨屋の会計のときは、割引のために、枝分かれする系統樹を盛り上げた。蔵書庫のときは、空かもしれない本を、幾重もの結界で囲った。地図のときは、座標を見張る伝令の網を、宙いっぱいに張った。そして今日は、報せ一つのために、工房を作る工房の塔を建てた。

並べて書いてみて、気づいてしまう。僕の手は、いつも、習った中でいちばん強い魔法、いちばん大きな仕掛けのほうへ、まっすぐ伸びていた。どこかで——強力な魔法ほど、偉い。そう、思い込んでいたのだ。そのたびに師匠は、たった一言で、僕をいちばん小さな道具のところへ、連れ戻した。

ペンを止めて、ふと思う。なぜ、僕の手は、いつも大きいほうへ伸びるのだろう。

次は——盛りたくなる、その手を止めて。誰かに言われるより先に、自分で一度、問うてみよう。「一番小さな道具で、足りないか」と。それができたとき、僕はきっと、ほんの少し、見習いから遠ざかる。


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

  • 修復した呪文(クラス/モジュール名): createNotification(生成を集めた呪文)と Notification 一族(文鳥便 MailFamiliar・早鐘 ShortBell・光触れ LightSign
  • 乱れの要因(アンチパターンの課題):
    • 「どの届け方を作るか」の振り分け(when)が、締切・招集など複数の窓口に、そっくり重複していた(散弾銃手術)。新種を足すには全窓口に書き足す必要があり、一箇所書き忘れて、急ぎの招集が光触れで出せず、討伐隊が集まらなかった。
  • 過剰設計の反省(背伸びの過ち):
    • 「究極の報せ生成陣」と意気込み、工房を作る工房の塔に、組立台(Builder)と写し台(Prototype)まで積んだ。だが種類ごとに専用の工房クラスが要るため、新種追加は「報せ+専用工房+上の工房の振り分け」と、かえって触る箇所が増え、一通送るのに階を上り下りするだけになった。第1部の背伸びが頂点に達した。宛先や本文を添える組立台は名前付き引数が、雛形の写し台は copy() が、言語の地力でもう肩代わりしていた。
  • 魔術の定石(学んだ設計パターン):
    • Factory Method。「ものを作る役目」を createNotification 一箇所に集め、各窓口は作って送るだけにした。新種追加はクラス1つ+集約の1分岐で、窓口は不変(修正に閉じ、拡張に開く=開放閉鎖の原則)。さらに届け方を sealed interface の型で表すと、種類を足して枝を書き忘れた瞬間にコンパイルで弾かれ、散弾銃手術の再発(書き忘れ)を型で封じられる。直したのは集約、sealed はそのだめ押し。
  • 模擬詠唱の記録(検証のポイント):
    • 各届け方が刷り見本どおり生成・送信されること(assertEqualsassertIs)、無い届け方は撥ねること(assertFailsWith)、新種を足しても窓口の呼び出しは一文字も変えずに通ること(局所性)を確認した。sealed 版は、コンパイルが通ること自体が「種類を漏れなく拾えている」証になる。
  • 師匠マグナスの格言:
    • 「“一番小さな道具で足りないか” と問うのが、いつでも先だよ。」
comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。