Featured image of post コードウィザード【Decorator】一杯のお茶に何を足せるか、組み合わせのたびにクラスが枝分かれして増え続ける〜トッピングを包んで重ねる札にし、変える所だけ書いて残りは委ねる〜

コードウィザード【Decorator】一杯のお茶に何を足せるか、組み合わせのたびにクラスが枝分かれして増え続ける〜トッピングを包んで重ねる札にし、変える所だけ書いて残りは委ねる〜

茶房のニナは、トッピングの組み合わせごとに品書きの札(クラス)が倍々に増えて参っていた。見習い魔術師ルークは継承の系統樹を中間流派で深掘りするが、交差する組み合わせの壁で手が止まる。師匠の問いから、包んで重ねる Decorator と Kotlin の by 委譲へ。Kotlin で描く。

札が、増えすぎた

あたしはニナ。アカデミーの麓町で、小さな茶房を一人で切り盛りしている。

看板は霊茶(れいちゃ)。薬草を煮出した、ほんのり甘くて体のあたたまるお茶だ。それに、お客の好みで甘味や香草を添える。ミルクを落としたり、蜂蜜をひとさじ垂らしたり。注文を受けると、品書きの魔法札が、その一杯のお代と、何が入っているかの品書きを、ちゃんと唱えてくれる仕掛けになっている。「霊茶、ミルク、五グルドと二グルドで七グルド」みたいに。お客は、お代と中身を聞いて頷いて、あたしは茶を淹れる。それだけの、小さな店。

茶房の朝は、湯気で始まる。薬缶がことこと鳴って、棚には茶葉の壺と蜜壺が並んでいる。そのあいだに、もう一つ。注文ごとの品書きの札を仕舞った、引き出しがある。

その引き出しが、近ごろ、閉まらない。

札で、溢れているのだ。

あたしは、お客の「いつもの」に、すぐ応えたい。一人ひとりの好みに合わせた一杯を、待たせずに出す。それが、この店の自慢だった。けれど、その一杯ごとに、札が一枚要る。そして札は、増えすぎた。

どうしてこんなことになったのか、数えてみると、よく分かる。

霊茶だけなら、札は一枚でいい。そこへ「ミルクを足せるように」「蜂蜜も足せるように」とした途端、札は三枚になった。ミルクだけの札、蜂蜜だけの札、そして——ミルクと蜂蜜の両方が入った札。「両方」の注文だって来るのだから、その組み合わせの札も、別に一枚要る。

それでも三枚なら、まだいい。問題は、そこへ香草を足そうとしたときだった。

香草だけの札。ミルクと香草の札。蜂蜜と香草の札。ミルクと蜂蜜と香草、ぜんぶ入りの札。——四枚、増える。三枚が、七枚になった。

書きながら、ぞっとした。じゃあ、次に生姜を足したら? いままでの七枚すべてに「生姜あり版」が要るから、七枚足して、十五枚。一品増えるごとに、札は倍に近く膨れていく。

あたしは、香草入りを品書きに載せたくて、必要な札を書き始めた。香草だけ、ミルク香草、蜂蜜香草……書いても書いても、終わらない。途中で、ペンを持つ手が止まった。

良かれと思って、お客の好みに、一つずつ応えてきたつもりだった。そのたびに、札を一枚ずつ、丁寧に。なのに気づいたら、自分で自分の首を、その札で、じわじわ絞めていた。

香草入りは、いまだに出せていない。それどころか、注文が来るたび、札の山から目当ての一枚を探すのに手間取って、お客を待たせるようになった。あたしの自慢の「すぐ出す」が、できなくなってきている。

一品、足したいだけなのに。なんで、こんなに——。

「あの子に、診てもらいな」

心当たりは、あった。

うちは、アカデミーの麓だ。見習いや学生が、よくお茶を飲みに来る。そのうちの一人、常連の学生さんが、札を探してまごつくあたしを見て、こう言ったのだ。

「ニナさん、品書きがそんなにごちゃごちゃで参ってるなら、丘のルークに診てもらいなよ。ああいう、ごちゃっとして手に負えなくなったやつを片すの、あの子、すごく得意だから」

ルーク。聞いたことのある名だった。何でも、見習いなのに、人の困りごとの「仕組み」を直すのがうまいらしい。藁にもすがる、というほど悲壮ではない。困ってるのはお互いさま、くらいの軽い気持ちで、あたしは札の束を風呂敷に包んで、丘を上った。

アカデミーの工房は、茶房とはまるで違う匂いがした。古い紙と、薬と、それから何か、ぴりっとした魔力の匂い。壁という壁が本棚で、宙には光の粒がちらちら浮いている。正直、あたしの目には、淹れたての一杯を出してやりたくなるくらい、雑然として見えた。根が世話好きなのだ。

その雑然の真ん中で、見習いらしい若者が一人、大きな魔導書を広げて、何か図を描いていた。枝分かれした、樹みたいな図だ。描いては、首をかしげ、ぐしゃっと消して、また描く。その繰り返し。何かを組み上げているというより、考えあぐねている、という風だった。

あたしが「あの、すみません」と声をかけると、彼のペン先が、ぴたりと止まった。描きかけの図をしばらく見つめてから、ようやく、こちらに視線を寄こす。まだ半分、頭が図のほうに残っているような、上の空の目だった。

「あ……いらっしゃい。って、お客さんじゃなくて、ご依頼ですか」

これが、噂のルークか。思っていたより、ずいぶん若い。あたしは風呂敷を解いて、札の束をどさっと机に置いた。

「うちの茶房の、品書きの札なの。一杯の種類が増えるたびに、こうやって札が一枚ずつ要って。組み合わせの分だけ、どんどん増えちゃって」

ルークは札を何枚か手に取って、めくりながら、ふんふんと頷いた。「なるほど。ちょっと、中を見せてもらいますね」。そう言って、彼は札の仕組みを、魔導式に書き起こしてくれた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// ニナさんの茶房の、今の品書き。
// トッピングの組み合わせごとに、別々の魔導式(クラス)を立てている。
open class HouseTea {                       // 看板の霊茶
    open fun cost() = 5
    open fun description() = "霊茶"
}

class TeaWithMilk : HouseTea() {            // ミルク入り
    override fun cost() = 5 + 2
    override fun description() = "霊茶, ミルク"
}

class TeaWithHoney : HouseTea() {           // 蜂蜜入り
    override fun cost() = 5 + 3
    override fun description() = "霊茶, 蜂蜜"
}

class TeaWithMilkAndHoney : HouseTea() {    // ミルクと蜂蜜・両方
    override fun cost() = 5 + 2 + 3
    override fun description() = "霊茶, ミルク, 蜂蜜"
}

ルークが、一つずつ指して説明してくれる。いちばん上の HouseTea が、看板の霊茶。cost がお代で、description が品書きだ(fun cost() = 5 のように、波括弧を使わず = の後ろに式を書くのは、その式の答えをそのまま返す省略形だという)。頭に付いた open は、「この魔導式は、引き継ぎを許しますよ」という印なのだという。

「この open があると、TeaWithMilk のように、HouseTea をもとにした新しい魔導式を作れるんです」

ある型を「〜の一種」として、親から丸ごと引き継ぐ。これを継承と呼ぶのだと、ルークは教えてくれた。霊茶を引き継いだミルク入りは、いわば霊茶の一種。だから、お代と品書きだけ、自分のやり方に書き換える。その「書き換えます」の宣言が、各メソッドの頭の override なのだそうだ。なるほど、ミルク入りは霊茶の親戚、というわけだ。あたしの感覚にも、すんなり馴染んだ。

「でも」と、ルークの指が、いちばん下の札で止まった。「問題は、こいつですね。ミルクと蜂蜜、両方入りの TeaWithMilkAndHoney。これも、わざわざ一枚、別に立ててある」

「そうなの」と、あたしは頷いた。「『両方ください』ってお客さんが来るから。その人用の札が、要るのよ」

「これは——トッピングの組み合わせごとに、別々の魔導式を立てている状態です」とルーク。「組み合わせの数だけ、札(クラス)が要る。こういうのには、ちゃんと名前があるんですよ」

機能の組み合わせごとに型を作ると、組み合わせの数だけ型が要り、種類を足すほど指数的に増えていく。この状態を組み合わせ爆発と呼ぶらしい。あたしの引き出しが閉まらないのにも、ちゃんと名前がついていたのか。

「一品足すたびに、倍に増えてくの」と、あたしは札の階段を、指で数えて見せた。「霊茶だけで一枚が、ミルクと蜂蜜で三枚。そこへ香草を足そうとしたら、もう七枚。……それで、書ききれなくて、手が止まっちゃって」

ルークは、札の山を見て、少し眉を寄せた。「香草、まだ出せていないんですね」

「うん。書ききれなくて」

流派を、もっときちんと

ルークは、しばらく札を眺めていた。それから、何か思いついたように、さっき描きかけていた魔導書の図に、目を戻した。

「組み合わせが多いなら——流派を、もっときちんと整理すればいいんです」

流派、とあたしが聞き返すと、彼は「継承の系統樹のことです」と言って、机の上に、中間の段を一つ、描き足し始めた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
abstract class Beverage {                    // すべての一杯の、おおもと
    abstract fun cost(): Int
    abstract fun description(): String
}

abstract class SweetBeverage : Beverage()    // 甘味の系統(ミルク・蜂蜜は、この下に)
abstract class SpicedBeverage : Beverage()   // 香りの系統(香草は、この下に)

class MilkTea : SweetBeverage()
class HoneyTea : SweetBeverage()
class MilkHoneyTea : SweetBeverage()

abstract と付いたのは、それ自体では一杯を出せない、骨組みだけの魔導式なのだとルークが言い添えた。霊茶のおおもとがあって、その下に「甘味の系統」と「香りの系統」を一段、挟む。甘味のものは甘味の系統の下に、香りのものは香りの系統の下に、きちんと仕分ける。

「ほら、こうすれば、整理できます」と、ルークはちょっと得意げだった。ただ、その得意げは、どこか控えめにも見えた。「これで何もかも解決だ」と胸を張るというより、「精緻に分ければ、捌けるはずだ」と、自分に言い聞かせるような。覚えたての継承を、もっとうまく使いこなしたい——そんな背伸びに、見えた。

確かに、甘味の系統の下に、ミルク、蜂蜜、ミルク蜂蜜が、すっと並んだ。あたしの目にも、いっとき、枝がきれいに揃って見えた。

「お、ちゃんと並んだ……ように見える」

でも、あたしは、すぐに引っかかった。茶房で札と格闘してきた、当事者の勘だ。

「待って、ルーク。それ……あたしがやってたのと、同じじゃない? 札を一枚ずつ増やしてたのを、もっとちゃんと、枝に分けただけ。きれいにはなったけど、増えるのは、変わらなくない?」

ルークは「いえ、これで整理が——」と言いかけて、香草を足そうとした。そこで、彼の手が止まった。

どっちの流派にも、置けない

香草は、香りのもの。だから、香りの系統の下に置けばいい。そこまでは、すんなりいった。

問題は、その次だった。「ミルクと香草、両方入りの一杯」は、どこに置けばいいのか。

ミルクは甘味の系統。香草は香りの系統。じゃあ「ミルク香草茶」は——甘味の系統の子なのか、香りの系統の子なのか。

1
class MilkKusaTea : ???   // 甘味の系統? それとも、香りの系統?

ルークは、図とにらめっこしたまま、動かなくなった。「ミルク香草茶は……どっちの流派に……」。さっきまで、すっきり並んでいた枝の前で、彼は明らかに、行き詰まっていた。

「Kotlinでは」と、ルークが、図を見つめたまま、低く呟いた。あたしに説明するというより、自分に言い聞かせるような声だった。「一つの魔導式は……親を、一つしか選べない。だから、甘味と香り、両方の系統の子には……できない」

一つのクラスは、引き継ぐ親を一つしか持てない。多重継承できない、というやつだ。だから、枝をどんなにきれいに、深く掘っても、二つの系統が交わる組み合わせは、その系統樹の、どこにも置き場がない。掛け算で増えていく組み合わせを、一本ずつ枝を伸ばす継承では、どうやっても捌けない。あたしが札を一枚ずつ書いて溺れたのと、根は、同じことだった。

そのときだった。工房の隅で、白髪の老人が一人、何か手仕事をしていた。乾かした薬草を、薄紙に一枚ずつ挟んで、本のように重ねている。その老人が、いま、手元の束の上に、薬草をもう一枚、すっと重ねた。それだけ。言葉は、何もない。

あたしは何気なく、その手元を目で追ったけれど、薬草を干している店番か何かだろうと、気にも留めなかった。

ルークは、描きかけの系統樹から、ふっと手を離した。そして、誰にともなく、低く呟いた。

「……なぜ、枝を増やすほど、苦しくなるんだろう」

あたしは、はっとした。

それ。それ、あたしが毎晩、札を書きながら、思ってたことだ。応えたい一心で、一枚ずつ増やして。なのに増えるほど、息が詰まっていく。あの感じ。

「……あたしと、同じだ」

つい、口に出していた。札に溺れた茶房の店主と、系統樹の前で立ち止まった見習いが、その一瞬だけ、ぴたりと重なった気がした。

一枚ずつ、重ねる手

隅の老人が、挟みかけの薬草に、薄紙を一枚そっとかぶせて、手を止めた。それから、系統樹に屈み込んだままのルークのほうへ、ゆったりと声を寄こした。

ルークが、振り向く。そして、少しあらたまった声で、「……お師匠さん」と、その人を呼んだ。

それで、あたしは察した。隅で黙々と薬草を重ねていた、あの人。ただの店番じゃない。この子の師匠——丘の上で名の知れた、大魔術師のマグナスさんだ。あたしはあわてて居住まいを正した。とんだ失礼をするところだった。

マグナスさんは、ルークの描いた系統樹には、目もくれなかった。ただ、手元の薬草の束を、ひょいと持ち上げて、宙に示すようにしながら、こう問うた。

「お前さんは、霊茶に香草を足すのに、新しい流派を一つ、興そうとしておる。——足すというのは、興すことかね? それとも、重ねることかね?」

それだけ言うと、マグナスさんは、また薬草を一枚、薄紙に挟んで、束の上に重ねる作業に戻ってしまった。答えは、言わない。さっき札の説明をしてくれたルークみたいに、手取り足取りで教える気は、どうやら、ないらしい。問いを一つ、ぽんと置いて、あとは待つ。そういう人のようだった。

ルークの目が、その、マグナスさんの手元に留まった。

薬草の束。一枚足しても、下の葉は、下の葉のまま。束は、解かれていない。ただ、上に、一枚、乗っただけ。

ルークは、はっと何かに打たれたように、机の上の、なんの飾りもない素の霊茶の札を、一枚つまみ上げた。そして、その上に、ミルクの札を、そっと——重ねて、かざした。

「——なんだ」と、彼は言った。「建て直さなくて、いいんだ。今ある一杯に、一枚、重ねれば」

包んで、委ねる

ルークは、描きかけの系統樹を、すうっと消した。あんなに悩んでいた枝が、跡形もなくなる。代わりに、彼は机の上に、新しい仕組みを書き始めた。

まず、一杯の「約束」を決める。

1
2
3
4
5
// 一杯の「約束」:お代と品書きを、必ず答える
interface Beverage {
    fun cost(): Int
    fun description(): String
}

「これは Beverage、一杯の約束です」とルーク。「『お代を訊かれたら答える、品書きを訊かれたら答える』。その約束だけを、先に決めておくんです」

interface——中身ではなく、「こう頼めば、こう応える」という形だけを取り決めた設計図のことだという。お客に「これ、なんのお茶?」「おいくら?」と訊かれたら、必ず答える。茶房の、当たり前の作法と、同じことだ。あたしにも、すぐ分かった。

その約束を、まず素の霊茶が守る。

1
2
3
4
5
// 看板の霊茶(素の一杯=土台)
class HouseTea : Beverage {
    override fun cost() = 5
    override fun description() = "霊茶"
}

「そして、ここからが、さっきと違うところです」とルーク。「トッピングを、霊茶の『一種』にするんじゃない。トッピングを、一杯を包む『飾り札』にするんです」

あたしは、さっきの行き詰まりを思い出して、訊いた。「ねえ、さっき。親は一つしか選べないって、止まってたよね。それは、もう、いいの?」

「いいんです」とルークは、少し嬉しそうに答えた。「さっきは、飾り札を霊茶の『一種』にしようとした。継承です。『一種』は、親を一つしか選べない。だから甘味系と香り系、両方の子になれずに、詰まった。でも今度は——飾り札が、霊茶を『土台に持つ』だけなんです」

土台に、持つ。あたしが、きょとんとしていると、ルークは言い換えてくれた。

「『〜の一種』じゃなくて、『〜を、土台に持つ』。継承の『一種』は、親を一つに決めなきゃいけない。でも、『持つ』なら——どの一杯でも、土台にできるんです。しかも、包んだ一杯を、また次の飾り札の土台にできる。だから、いくつでも重ねられる」

「〜の一種」(継承)ではなく、「〜を土台に持つ」。この関係を合成(has-a=「持っている」)と呼ぶらしい。継承は親を一つに決め打つけれど、合成は、どの一杯でも土台にできて、包んだそばから、また次の土台になる。一枚の飾り札が持つ土台は一つでも、その入れ子をたどれば、いくつでも重なる——これが、あたしの札の爆発を、解いてくれるのだという。

その「持つ」を、魔導式にすると、こうなる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 飾り札の土台:変えない仕事は、土台へまかせる
abstract class Topping(private val base: Beverage) : Beverage by base

// ミルクの飾り札:変える所(お代・品書き)だけ書く
class Milk(private val base: Beverage) : Topping(base) {
    override fun cost() = base.cost() + 2
    override fun description() = base.description() + ", ミルク"
}

// 蜂蜜の飾り札
class Honey(private val base: Beverage) : Topping(base) {
    override fun cost() = base.cost() + 3
    override fun description() = base.description() + ", 蜂蜜"
}

飾り札は、丸括弧の中で base——土台になる一杯——を受け取る。「丸括弧に private val と書いておくと、受け取った一杯は、その飾り札の持ち物になる」のだと、ルークが言い添えた。ミルクの飾り札は、土台の一杯を一つ抱えて、そのお代に二グルド足し、品書きに「ミルク」を書き足す。それだけ。

Topping のうしろにある by base という見慣れない一言は、ひとまず「変えない仕事は、土台にまかせる」という印だと思っておけばいい、とルークは言った。その中身は、次の節で詳しく見せてもらうことになる。

ただ、あたしには、もう一つ引っかかりがあった。「ねえ。Milk: Topping って……それ、さっきやめたはずの継承じゃないの?」。ルークは頷いた。「鋭いですね。継承は、ここでも使っています。でも、使いどころが、さっきと違うんです。さっきは『ミルク蜂蜜』みたいな組み合わせそのものを、継承で一枚ずつ表していた。それが、爆発の元でした。今度は、組み合わせは base に土台を持って、包む順で表す。継承しているのは、どの飾り札も同じ『土台にまかせる』骨組みを、使い回すためだけなんです」

「こうやって、一杯を飾り札で包んで、機能を後から重ねていくんです。継承の代わりに使える、拡張のやり方ですよ」

オブジェクトを、同じ形の「飾り」で包んで、機能を後から重ねて足す。この定石をDecoratorと呼ぶのだと、ルークは教えてくれた。新しい流派を興すのでも、本体を建て直すのでもなく、今ある一杯に、薄い飾りを、一枚ずつ。

ルークは、空中に光のペンで、さっきの枝分かれの図と、新しく作った包み込む図を並べて描き出した。

「さっき僕がやろうとして行き詰まった『継承の枝分かれ』と、この『包んで重ねる仕組み』を並べると、こうなります」

Decorator pattern class and object structure showing how components wrap each other

「なるほど……」と、あたしは光の図を見つめた。「これなら、どんなにトッピングを重ねても、新しい札を切り出す必要がないわけだね。ただ、外側に新しい皮を一枚、重ねて包むだけでいいんだから」

「はい!」とルークは頷いた。「ロシアの入れ子人形(マトリョーシカ)みたいに外側からすっぽり包み込んでいく構造です。お代や品書きは、外側から内側へと順に委ねられ、計算しながら戻ってきます」

包む、というのは、こういうことだ。

1
2
Milk(HouseTea())               // 霊茶を、ミルクの飾り札で包んだ一杯
Honey(Milk(HouseTea()))        // それを、さらに蜂蜜で包んだ一杯

素の霊茶を、ミルクで包む。それを、さらに蜂蜜で包む。組み合わせは、新しい札を立てるんじゃなく、包む順で、表す。

そこで、あたしは、あっと声を上げた。

「あ……さっき。お師匠さんが、薬草を一枚、上に重ねてたみたいに」

ルークが、ちょっと驚いた顔で、あたしを見た。それから、隅のマグナスさんのほうを、ちらりと見て、小さく頷いた。

「ええ。たぶん、そういうことです」

書き写しは、まかせる

「飾り札は、変える所——お代と品書き——だけ書きます」とルーク。「それ以外の仕事は、土台に、まかせる。委ねるんです」

あたしには、その「委ねる」が、まだピンと来なかった。ルークは、少し考えて、こう説明してくれた。

「本当なら、飾り札の土台に、こう書かなきゃいけないんです。『お代を訊かれたら、土台に訊いて、その答えを返す。品書きを訊かれたら、土台に訊いて、その答えを返す』——と、一つずつ、手で書き写す」

1
2
3
4
5
// もし「まかせる」を、手で書くと……(書き写しが要る)
abstract class ToppingManual(private val base: Beverage) : Beverage {
    override fun cost() = base.cost()               // 土台に訊いて、返すだけ
    override fun description() = base.description() // 土台に訊いて、返すだけ
}

あたしは、その「書き写し」という言葉に、引っかかった。

「……それって。あたしが、組み合わせごとに、似たような札を、何枚も書き写してたのと、似てる?」

「ええ、まさに」とルーク。「その書き写しが、面倒の元なんです。だから——」

彼は、さっきの飾り札の土台の、Beverage by base という、見慣れない一言を指した。

「そこで、by です。これを付けると——変えていないメソッドの分だけ、魔導具が、術を編むときに、『土台に訊いて、返す』という書き写しを、ぜんぶ自動で書き足してくれるんです」

魔導具、というのは、Kotlinのことらしい。この by というのが、by インタフェース委譲——訊かれた仕事を土台へそのまま通す書き写しを、変えていないメソッドの分だけ、コンパイラ(魔導具)が自動で書いてくれる仕掛け——なのだという。だから飾り札には、変える所——お代と品書き——だけ書けば、それでいい。さっきの「手で書き写す」版が、by の一言で、まるごと要らなくなる。

とはいえ、と、あたしは Milk の魔導式を見直した。お代も品書きも、両方とも書き換えている。約束が、この二つきりだからだ。じゃあ by は、いったい何を省いてくれたのか。ルークの指が、さっき手で書いた ToppingManual の、土台へ通すだけのあの二行に戻った。「by が消したのは、ここです。飾り札の土台——Topping——が、本来こう書かなきゃいけなかった転送を、まるごと。飾り札そのものが書く量は、変わりません」。なるほど。省けたのは、飾り札の表側じゃなく、その土台のほうだったのだ。

「じゃあ」と、あたしは身を乗り出した。いちばん訊きたいことだった。「新しいトッピングを足すときは、どうなるの?」

「飾り札を、一枚、書くだけです。今ある札は——ミルクも、蜂蜜も、素の霊茶も——一枚も、書き直しません」

1
2
3
4
5
6
7
8
9
// 新しいトッピングは、この飾り札を一枚足すだけ。
// 既存の札(Milk / Honey / HouseTea)は、一枚も書き直さない。
class Kusa(private val base: Beverage) : Topping(base) {
    override fun cost() = base.cost() + 1
    override fun description() = base.description() + ", 香草"
}

// 任意の組み合わせは、包む順に重ねるだけ。新しいクラスは要らない。
val drink: Beverage = Kusa(Milk(HouseTea()))   // 霊茶+ミルク+香草、お代 5+2+1=8

「……あの」と、あたしは、声が少し震えた。「あたしが、作っても作っても、終わらなかった香草が……この、一枚で?」

「はい。香草の飾り札を、一枚。あとは、好きな順に重ねるだけ。ミルク香草も、蜂蜜香草も、ぜんぶ入りも、もう、別々の札は要りません」

引き出しに溢れた、あの札の山。あれが、一枚で。

「もし、この先」とルークは続けた。「約束に、お代と品書きのほかにも、訊くことが増えても——たとえば『何分蒸らすか』とか——飾り札は、書き直さなくていいんです。増えたぶんも、土台に委ねるだけだから」

試しに、淹れてみる

「いきなりお客にお出しする前に、ここで何杯か、淹れてみましょう」

ルークはそう言って、机の上に、小さな結界を張った。狙いどおりのお代と品書きが出るか、一杯ずつ、あたしの目の前で確かめるためだ。本番の前に結界の中で試すこの作法を、魔術師は模擬詠唱と呼ぶらしい。

 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
import kotlin.test.assertEquals

fun main() {
    // 素の一杯
    assertEquals(5, HouseTea().cost())
    assertEquals("霊茶", HouseTea().description())

    // 一重ね(ミルク)
    assertEquals(7, Milk(HouseTea()).cost())
    assertEquals("霊茶, ミルク", Milk(HouseTea()).description())

    // 三重ね(ミルク+蜂蜜+香草)
    assertEquals(11, Kusa(Honey(Milk(HouseTea()))).cost())
    assertEquals("霊茶, ミルク, 蜂蜜, 香草", Kusa(Honey(Milk(HouseTea()))).description())

    // 重ねる順で、品書きの並びは変わる。でも、お代は同じ
    assertEquals("霊茶, ミルク, 蜂蜜", Honey(Milk(HouseTea())).description())
    assertEquals("霊茶, 蜂蜜, ミルク", Milk(Honey(HouseTea())).description())
    assertEquals(10, Honey(Milk(HouseTea())).cost())
    assertEquals(10, Milk(Honey(HouseTea())).cost())

    // 香草を一枚足すだけで、既存の札を書き直さず作れる
    assertEquals(8, Kusa(Milk(HouseTea())).cost())

    println("ALL TESTS PASSED")
}

assertEquals は、淹れた一杯のお代が、品書きどおりか——その二つを、突き合わせて見る道具だという。品書きのほうも同じで、一杯に何が入っているかの書き出しが、狙いどおりかを、一文字ずつ見比べる。

あたしの目の前で、一杯ずつ、答えが出た。

素の霊茶は、五。ミルクを重ねて、七。蜂蜜と香草も重ねた三重ねは、五に二と三と一を足して、十一。どれも、ぴたり。

それから、ルークは、おもしろいものを見せてくれた。蜂蜜を先に重ねた一杯と、ミルクを先に重ねた一杯。品書きの並びは、「ミルク, 蜂蜜」と「蜂蜜, ミルク」で、ちゃんと変わる。包んだ順に、書き出されるからだ。でも、お代は、どちらも十。足し算だから、足す順番が違っても、合計は同じ。「並びは変わるけど、お代は変わらない」。茶房の感覚にも、しっくりきた。

そして、最後の一つ。香草の飾り札を一枚足しただけで、ミルク香草の一杯が、ちゃんと淹さって、お代も八と出た。あたしが、書ききれずに溺れた、あの組み合わせが。既存の札には、指一本、触れていないのに。

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

一枚で、ぜんぶ

整え直された、一杯の仕組みを、あたしは受け取った。

引き出しに溢れていた、あの札の山は、もう要らない。残ったのは、素の霊茶の札が一枚と、トッピングの飾り札が、ミルク、蜂蜜、香草……数えるほど。それだけ。

「これだけ……? これだけで、ぜんぶの組み合わせが、出せるの?」

「出せます」とルークは頷いた。「組み合わせは、札の数じゃなく、重ねる順で表しますから」

あたしは、しばらく、その数枚の札を、ただ見ていた。札の山と格闘して、目当ての一枚が見つからなくて、お客を待たせて。香草が書ききれなくて、手が止まって。あの夜たちが、嘘みたいに、軽くなっていた。

お客の好みに応えるたびに、札が増えて、首が絞まっていく気がしていた。だから、どこかで、こう思いかけていた。あたしが、欲張って、応えすぎたんだ、と。

でも、違ったのだ。

お客の「あれも、これも」に応えるのは、悪いことじゃなかった。ただ、応え方が——札を一枚ずつ増やすやり方が、違っただけ。盛り続けた日々が、否定されたわけじゃない。そう思えたことが、あたしには、何より嬉しかった。

盛るんじゃなく、重ねて

あたしは、自分の手で、試してみたくなった。

新しいトッピング。そうだ、生姜。冬のお客に出してやりたくて、ずっと諦めていたやつだ。あたしは、ルークに教わったとおり、生姜の飾り札を、一枚だけ書いた。お代に一グルド、品書きに「生姜」。それだけ。

そうしたら——ミルク生姜も、蜂蜜生姜も、ミルク蜂蜜生姜も、ぜんぶ、出せるようになっていた。既存の札は、本当に、一枚も書き直していないのに。生姜の札を、重ねるだけ。

ほんの少し前の、あたしだったら。生姜を足すのに、今ある札ぜんぶに「生姜あり版」を書き足して、また溺れていたはずだ。それが、一枚で、済んだ。

「足すって……こんなに、身軽だったんだ」

盛るのに苦しんでいたのが、嘘みたいだった。盛るんじゃなく、重ねる。たったそれだけで、こんなに違う。

奥のマグナスさんが、薬草の束を、また一枚、上に重ねながら、ぽつりと言った。

「ほら。一枚足しても、束は解けておらん。下の葉は、下の葉のまま。上に、一枚、乗っただけ。——魔術も、同じだよ」

あたしは、帰り際に、ふと思った。

あの子は——ルークは、お師匠さんに、答えを教わったわけじゃない。お師匠さんは、ただ、薬草を重ねながら、問いを一つ、置いただけだった。「足すというのは、興すことかね、重ねることかね」と。あの子は、その問いを手がかりに、自分で「建て直さなくていい」に、辿り着いた。

工房に着いたとき、あの子が、魔導書に枝の図を描いては消し、描いては消していた、あの考え込む横顔を思い出す。きっと、あのときも、ずっと、自分に問うていたんだ。「なぜ、枝を増やすほど、苦しいんだろう」って。

茶房へ帰る道は、来たときより、ずっと軽かった。あんなに重かった札の束を、もう、抱えなくていい。引き出しは、すんなり閉まるだろう。

これからは、お客の「あれも、これも」に、身軽に応えられる。盛るんじゃなく——重ねて。


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

  • 修復した呪文(クラス/モジュール名): Beverage(一杯の約束)と、素の HouseTea、飾り札の土台 Topping、各トッピング Milk / Honey / Kusa
  • 乱れの要因(アンチパターンの課題):
    • トッピングの組み合わせを、継承(流派の系統樹)で表していた。組み合わせの数だけクラスが要り、ミルク・蜂蜜の二品で三クラス、香草を足せば七、もう一品で十五……と倍々に膨れた(組み合わせ爆発)。しかも、甘味系と香り系が交わる組み合わせ(ミルク香草)は、クラスの親を一つしか選べない(多重継承できない)ため、系統樹のどこにも置けなかった。
  • 過剰設計の反省(背伸びの過ち):
    • 継承の系統樹を「もっと精緻に」と、中間の流派(甘味系・香り系)で深掘りしようとした。一軸(甘味だけ)なら整って見えたが、二軸目(香り)が交わった瞬間、置き場が消えて手が止まった。枝をどれだけ深く掘っても、掛け算の組み合わせは、一本ずつ伸ばす継承では捌けない——師匠の問いを手がかりに、自分でそう気づけた。建て直す必要は、なかった。
  • 魔術の定石(学んだ設計パターン):
    • Decorator。一杯を、同じ約束(interface)を守る飾り札で包み、機能を後から重ねる。型はトッピングの数だけ(足し算)で済み、組み合わせはネスト(包む順)で表す。継承(is-a=〜の一種)ではなく、合成(has-a=〜を土台に持つ)にしたことで、土台は何枚でも重ねられ、爆発が解けた。Kotlin の by インタフェース委譲で、変えない仕事の「書き写し(転送)」をコンパイラに自動生成させ、飾り札は変える所(お代・品書き)だけを書く。なお by 委譲には、土台が内部で自分の別メソッドを呼ぶ場合に飾り札の上書きがそこへ届かない、という癖もある。今回は各トッピングが独立にお代と品書きを足すだけなので、問題にはならない。
  • 模擬詠唱の記録(検証のポイント):
    • 素・一重ね・三重ねの、お代と品書きが狙いどおりであること、重ねる順で品書きの並びは変わるがお代は同じ(足し算ゆえ順不同)であること、そして新しいトッピング(香草)を一枚足すだけで、既存のクラスを一切書き直さずに任意の組み合わせが作れること(局所性=爆発が消えた一点)を確認した。
  • 師匠マグナスの格言:
    • 「一枚足しても、束は解けぬ。下の葉は、下の葉のまま。上に一枚、乗るだけだよ。」
comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。