Featured image of post コードウィザード【Adapter】外部の観測所は作り変えられないのに、無理に一つの親から継承させようとして行き詰まる〜継承をやめ、間に立つ変換役を挟んで共通の約束に翻訳する〜

コードウィザード【Adapter】外部の観測所は作り変えられないのに、無理に一つの親から継承させようとして行き詰まる〜継承をやめ、間に立つ変換役を挟んで共通の約束に翻訳する〜

飛空艇の運航掛ハルは、観測所ごとに形の違う天候の報せを、航路の判断が一つずつ読み分ける密結合に悩む。見習い魔術師ルークは共通の親から継承させようとして外部の壁にぶつかるが、師の問いから、間に通訳を立てて共通の形へ訳すAdapterへ。Kotlinの委譲で描く。

観測所の言葉は、みんな違う

俺はハル。飛空艇の運航掛だ。風に乗って空を渡る船を、今日は出すか、それとも見合わせるか。それを決めるのが俺の仕事さ。

空を出すかどうかは、天気しだいだ。だが、空ってのは広い。出発の空が晴れていても、道中で嵐に突っ込んじゃ元も子もない。だから俺は、航路の途中に建つ、いくつもの観測所から天候の報せを取り寄せている。

厄介なのは、その観測所が、どれもてんでばらばらの言葉で報せを寄こすってことだ。

「空目の塔」は、言葉で寄こす。「晴」「曇」「嵐」、そのどれかだ。「雲見の計」は、数字で寄こす。雲の量を、0から10までの目盛りで。0がいちばん晴れていて、10で空一面が雲、って寸法だ。近ごろ増える話が出てる「絵符所」に至っては、絵符——絵札で寄こすらしい。

観測所は、俺のもんじゃない。よその魔術師ギルドが建てて、あっちの流儀で動かしている、出来合いの仕掛けだ。こっちで中身を作り変えることはできない。だから俺は、そいつらの寄こす形を、そのつど頭の中で訳して、「この航路は出せるか」を決めている。

長いことこの稼業をやってきたが、この「訳す」ってのが、じわじわ効いてくる。相手の形は変えられない。なのに、観測所が増えるたび、俺のほうが相手の数だけ読み方を覚えて、航路の数だけ、それを書いて回っている。……どうせ相手は変わらねえのに、な。

同じ空を、逆に読んだ日

いつだったか、「雲見の計」を新しく繋ぎ込んだときのことだ。

北風路の判断には、こう書いてあった。「雲量が3以下なら、晴れ扱いにして出す」。俺はそれを、南風路の判断にも、手で書き写した。同じ観測所なんだから、同じ読み方でいい。そのはずだった。

ところが俺は、写すときに、うっかり逆に読んだ。「数字は大きいほど天気がいい」と思い込んでな。気づかないまま、ほとんど快晴の日に、南風路の便を一つ、欠航にしていた。あとで気づいて、こっそり直した。大きな事故ってわけじゃない。ただの、静かな取りこぼしさ。

だが、これが骨身に染みた。同じ読み方を、あっちの判断にもこっちの判断にも手で書き写している限り、こういう写し損じは、いつか必ず紛れ込む。

いま、俺の運航判断は、こんなふうになっている。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 外部の観測所(提供元)。よそのギルドが建てた仕掛けで、こちらの魔導書の外にある=中身に手を入れられない
class SkyEyeStation(private val skyValue: String = "晴") {   // 空目の塔:言葉で返す
    fun sky(): String = skyValue                              // "晴" / "曇" / "嵐"
}

class CloudGauge(private val level: Int = 1) {                // 雲見の計:数字で返す(0=快晴 … 10=全天曇)
    fun cloudLevel(): Int = level
}

// 北風路の運航判断
fun canFlyNorthRoute(station: Any): Boolean = when (station) {
    is SkyEyeStation -> station.sky() == "晴"
    is CloudGauge    -> station.cloudLevel() <= 3     // 雲見の計の流儀:3以下なら晴れ扱い
    else -> throw IllegalArgumentException("知らない観測所です")
}

// 南風路の運航判断(北とそっくり同じ読み替えを、手で写している)
fun canFlySouthRoute(station: Any): Boolean = when (station) {
    is SkyEyeStation -> station.sky() == "晴"
    is CloudGauge    -> station.cloudLevel() <= 3
    else -> throw IllegalArgumentException("知らない観測所です")
}

引き取り手は、Any という、いちばん大雑把な型で観測所を受けている。Any は、Kotlin で「どんな型でも受け取れる、いちばん大元の型」だ。何でも受けられる代わりに、受けたあとで「こいつは空目の塔か? それとも雲見の計か?」と、is で型を確かめ直さなきゃならない。is で確かめると、その分岐の中では、Kotlin が自動でその型として扱ってくれる(スマートキャスト、という)。だから、is CloudGauge と確かめた枝の中では、雲見の計ならではの station.cloudLevel() が呼べる。

そして、その確かめと読み替えが、北風路にも南風路にも、そっくり書き写してある。観測所の言葉を訳す役目を、運航判断のほうが、まるごと抱え込んでいるわけだ。

近く、俺は「絵符所」をもう一つ繋ぎ込むことになりそうだ。だがこの作りだと、絵符所の読み替えを、北風路にも南風路にも、また書き足さなきゃならない。どこか一つ書き忘れれば、その航路だけ、絵符所を「知らない観測所です」と撥ねてしまう。

観測所が一つ増えるたびに、航路の判断を全部開いて、同じことを書いて回る。……いい加減、うんざりしていた。

丘の見習いを、訪ねてみるか

各地を渡る稼業をしていると、いろんな噂が耳に入る。ここしばらく、寄港地のあちこちで、同じ名前を聞くようになっていた。

「アカデミーの丘に、こんがらがった魔導式をほどくのが得意な見習いがいる」。丘のルーク、というらしい。誰か一人が言うんじゃない。港から港へ、その名が風に乗るみたいに広がっていた。

藁にもすがる、って歳でもない。ただ、そこまで名が届くなら、一度、見ておくかと思った。俺は船を一日休ませて、アカデミーの丘を上った。

工房は、本棚と、宙にふわふわ浮かぶ光の粒でいっぱいだった。まあ、魔術師の部屋なんてこんなもんだろう。各地で妙なものは見飽きている。俺が目を留めたのは、部屋の隅にいた白髪の老人のほうだ。

あれは——たぶん、大魔術師マグナス様だろう。その風貌も、遠くの港でまで語り草になっている。会うのは初めてだが、見ればわかる。老人は、こちらには目もくれず、異国の文字が綴られた札を一枚手に取っては、見慣れた文字の帳面へ、一行ずつ、静かに書き写していた。

肝心の見習いは、机のほうにいた。形も大きさもばらばらな、いくつもの鍵を前にして、一本ずつ、細い添え金みたいなものを鍵に宛てがっては、小さな鍵箱に差し込もうとしている。「この鍵は……歯が違う。削るんじゃなくて、間に、これを……」と、独りごちながら。

「よう」と俺が声をかけると、鍵を置いて顔を上げた。「あ……観測所の、報せのご相談ですか」。これが噂のルークか、と俺は胸の内で品定めした。まだ若い。

俺は、持ってきた運航判断の写しを見せて、事情を話した。前に一度、同じ空を逆に読んで便を欠航させたこと。近く絵符所が増えて、また航路ぜんぶに書き足すことになりそうなこと。

ルークは写しをじっと見て、こう言った。「これは……観測所ごとの"形"を、航路の判断のほうが、直に読み分けていますね。しかも、同じ読み替えが、北にも南にも、そっくり書き写してある」。それから、少し眉を寄せた。「だから、新しい観測所を一つ増やすと、北も南も、同じ書き足しをして回ることになる。どこか一つ忘れれば、その航路だけ、その観測所を撥ねてしまう」。

「そう、それだ」と俺は頷いた。「それが、うんざりなんだよ。観測所の言葉が、みんな違うんだ。俺が、そのつど訳してる」。

ルークが説明してくれたところによると、これは 不整合インタフェースへの個別対応 ——相手ごとに形の違う仕掛けへ、呼び出す側が一つずつ手で合わせにいく、絡まりやすい作りなんだそうだ。相手が増えるほど、合わせる側がふくれ上がっていく。

すべての観測所を、一つの親から

ルークは、しばらく写しを見つめてから、ぱっと顔を上げた。

「なるほど……なら、すべての観測所を、一つの親から、生えさせましょう」。

親、というのは、魔導式の世界の言葉で「継承」——親の魔導式を、子がまるごと引き継ぐ仕組みのことらしい。ルークは筆を走らせ、こんな骨組みを描いてみせた。

1
2
3
4
// ルーク「すべての観測所を、一つの親(共通の基となる魔導式)から、生えさせましょう」
abstract class WeatherStationBase {
    abstract fun report(): WeatherReport   // report()=共通の"読み"を返す口(WeatherReport の中身は後で決める)。皆これを継承すれば揃う……という発想
}

「この親に、report という共通の口を持たせます。空目の塔も、雲見の計も、みんなこの親を継げば、同じ口で答えるようになる。そうすれば、航路の判断は、もう形の違いに悩まなくて済みます」。

「ほう」と俺は少し期待した。「みんなが、同じ口で答えるようになる、と」。

ところが、ルークの手が、途中で止まった。

「……継がせられない」。声が、しぼんだ。「この空目の塔は、僕らの魔導書の外にあるんです。よそのギルドが建てた仕掛けで、宣言そのものに、後から親を足せない。手を入れられないんです」。

1
2
3
4
5
6
// だが、観測所(SkyEyeStation・CloudGauge 等)は「外部の仕組み」——こちらのソースではない。
// 後から親を継がせようとしても、外部のクラス宣言そのものを書き換えることはできない
// (Kotlin のクラスは既定で final・かつ他ギルドが所有する型のため手を入れられない)。
//
// class SkyEyeStation : WeatherStationBase() { ... }
//   ↑ この行は、そもそも書けない(=継承ルートが最初から塞がれている)。

ルークの継承の図は、外部の観測所の手前で、線が繋がらないまま宙に浮いていた。

「そりゃそうだ」と俺は言った。「観測所は、俺たちのもんじゃない。あっちの都合で、あっちの形で寄こす。こっちで作り変えられるなら、最初から苦労してねえよ」。

ルークは一度、食い下がった。「なら、観測所の写しを作って、その写しに親を継がせて……」。だが、口にしながら自分でも気づいたようだった。写しを作れば、元の観測所と二重に世話をすることになる。かえって絡まる。手が、また止まった。

隅では、マグナス様が、あいかわらず異国の札を帳面へ写し続けていた。羽ペンの走る、かすかな音だけが続いている。俺には、それが大魔術師の手すさびに見えた。何をしているのかは、そのときは気にも留めなかった。

相手を、変えようとしてた

ルークは、宙で途切れた継承の線を、目でじっと辿っていた。そして、低い声で呟いた。俺に言ったんじゃない。自分に、確かめるみたいに。

「継がせようとしていた。皆を、一つの親に……でも、観測所は、僕らのものじゃない。変えられない」。

そこで、言葉が途切れた。

「……相手を、変えようとしてた」。

ルークの手が動いた。宙に浮いた継承の線を、消しはしなかった。その線と、外部の観測所との、ちょうど間に——小さな四角の枠を、一つ、描き足したんだ。さっき、鍵に添え金を宛てがっていた、あの手つきと同じだった。

「相手を変えられないなら……間に、通訳を立てればいい」。ルークの声に、少しずつ張りが戻ってきた。「観測所はそのまま。こっちの言葉に訳す役を、間に、一つずつ挟むんです」。

俺が、この稼業でずっと頭の中でやってきた"通訳"を、この子は、形にしようとしている。そう思った。

書き直せとは、言えぬ

ルークが、確かめるように、隅のマグナス様のほうへ目をやった。

マグナス様は、書き写す手を止めなかった。ただ、手元の帳面を、こちらへ少しだけ傾けて見せた。異国の文字が並んだ札と、見慣れた文字が並んだ帳面が、隣り合っている。

「わしはな」と、マグナス様は写しながら言った。「この文(ふみ)の主(あるじ)に、『書き直せ』とは言えぬ。わしの言葉で書け、とも言えぬ。相手の国の筆は、相手のものだからな」。羽ペンが、また一行を写す。「じゃが——間に一枚、両方が読める帳面を挟めば、それで用は足りる。さて、お前さんの観測所は、どうだろうね?」。

それだけ言うと、マグナス様は、また次の札を手に取り、写す作業に戻ってしまった。答えは、言わない。

ルークの目が、開いた。

「……そうか。観測所を継がせて、作り変えるんじゃない。観測所は、そのまま。間に、両方が読める役を、一つずつ立てる。相手を変えずに、形だけ訳す」。

マグナス様は、答えを教えたわけじゃなかった。ただ、問いを一つ、置いていっただけだ。あとの道は、ルークが自分の足で辿り着いた。俺には、そう見えた。

間に立つ、変換役を一つずつ

ルークは、新しい魔導式を組み始めた。

「まず、航路の判断が期待する、たった一つの共通の約束を決めます」。

1
2
3
4
5
6
7
8
9
// 共通の"読み"(正規化された天候)。取りうる姿を名前で出し切った型
enum class Condition { CLEAR, CLOUDY, STORM, UNKNOWN }   // UNKNOWN=観測不能/欠測

data class WeatherReport(val condition: Condition)

// 呼び出し側(航路判断)が期待する、ただ一つの共通の約束
interface WeatherSource {
    fun report(): WeatherReport
}

Condition は、enum class ——取りうる値を、あらかじめ名前で出し切った型です。空模様を『晴れ(CLEAR)』『曇り(CLOUDY)』『嵐(STORM)』、それに『観測できず(UNKNOWN)』の四つに揃えました。WeatherReport は、その読みを運ぶ器です」。

「そして WeatherSource が、共通の約束。『report と呼べば、共通の形の天候が返ってくる』という取り決めです。空目の塔も雲見の計も、この約束そのものは、守っていません。だから——」。

そこでルークは、各観測所ごとの通訳を、一つずつ書いていった。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 各観測所に「間の通訳(変換役)」を一つずつ立てる(Object Adapter=相手を保持して委譲)
class SkyEyeAdapter(private val station: SkyEyeStation) : WeatherSource {
    override fun report(): WeatherReport = WeatherReport(
        when (station.sky()) {
            "晴" -> Condition.CLEAR
            "曇" -> Condition.CLOUDY
            "嵐" -> Condition.STORM
            else -> Condition.UNKNOWN
        }
    )
}

class CloudGaugeAdapter(private val gauge: CloudGauge) : WeatherSource {
    // 「雲量3以下なら晴れ」——0=快晴という"雲見の計だけの流儀"を、この一箇所だけが知る
    override fun report(): WeatherReport =
        WeatherReport(if (gauge.cloudLevel() <= 3) Condition.CLEAR else Condition.CLOUDY)
}

この、間に立つ変換役こそが、Adapter ——既存の仕掛けを、目的の形の口へ翻訳し、形の違いを吸収する定石だ、とルークは言った。

「その約束を、観測所に継がせるんじゃないのか」と俺は訊いた。

「継がせません」とルーク。「継がせられなかった、というのもありますが……そもそも、継ぐ必要がないんです。ここでは、観測所を『継承する』——つまり相手に"なる"んじゃなく、private val station として、相手を手元に"持って"使っています。これを「委譲」と言います。相手を継いで自分がその一族になるんじゃなく、相手を懐に抱えて、その言葉を訳す」。

「持って、使うだけ、か」。

「はい。だから、相手が外部の仕掛けで、こちらから継がせられなくても、まったく困りません。懐に持つだけなら、相手の都合は関係ありませんから」。

俺には魔術のことはわからないが、そこは腑に落ちた。相手を身内にしようとするから、相手の戸籍がどうこうって話になる。ただ懐に抱えて通訳するだけなら、相手が何者だろうと構わない、というわけだ。

ルークは、少し独りごとのように付け足した。「前に、別のご依頼で、同じ約束を守ったまま機能を"重ねる"やり方をやったんです。あれは、包む側も中身も、同じ約束を守っていたから、by という書き方で、いじらない部分を土台へまるごと預けられた。……でも今回は、違う。相手は、そもそも同じ約束を守っていない。だから、まるごとは預けられません。中の、訳す手続きを、一つずつ手で書くしかない。それが、“訳す"ということなんです」。

なぜ、真偽ではなく"読み"を返すのか

一つ、気になったことがあった。共通の約束が返すのは、天気の"読み”(WeatherReport)だ。俺が知りたいのは結局「出せるか、出せないか」なんだから、いっそ約束のほうで真偽——出せる/出せない——を返せば早いんじゃないか。俺はそう訊いてみた。

ルークは首を振った。「それだと、この通訳が、ただの『出せるか判定を差し替える部品』に見えてしまうんです。差し替えるだけの部品は、別の定石(Strategy——判定のやり方そのものを、丸ごと差し替えるための定石)と紛らわしい。今回やりたいのは、判定の差し替えじゃなくて、ばらばらの"形"を、共通の"形"に訳すことです。だから、真偽じゃなく"読み"を返す。そうすれば、これが翻訳役だと、はっきりします」。

なるほど、と俺は思った。この子は、名前のためにパターンを選んでるんじゃない。何をしたいか、で選んでいる。

単純な訳は、拡張関数で

「雲見の訳は、数字を読み替えるだけなので」とルーク。「Kotlin の 拡張関数 ——既にある型に、後付けで見かけの口を生やす書き方——を使えば、cloudLevel().toCondition() のように、もっと読みやすくもできます。ただ、これはあちこちに撒くと、“見えない口出し"になって追いづらくなるので、この通訳の中だけに、そっと留めておきます」。

判断は、共通の約束だけを見る

最後に、ルークは、航路の判断そのものを、こう書き直した。

1
2
// 運航判断は、共通の約束だけを見る(もう各観測所の"形"を知らない)
fun canFly(source: WeatherSource): Boolean = source.report().condition == Condition.CLEAR

「航路の判断は、もう観測所の形を知りません。共通の約束だけを見て、『共通の読みが晴れ(CLEAR)なら出す』。北風路も、南風路も、これと同じ一行で済みます」。

「待て」と俺は口を挟んだ。「読み替えは、どこへ行った。俺が航路ごとに書いてた、あの『3以下なら晴れ』ってやつだ」。

「雲見の通訳、一箇所だけに集めました」とルーク。さっき書いた CloudGaugeAdapter を指す。「『3以下なら晴れ扱い』——雲見の計だけの、あの流儀を知っているのは、もうこの一箇所だけです。航路には、もう写しません」。

「……写す先が、無くなったのか」。

俺は、あの静かな欠航を思い出していた。前に俺が写し間違えたのも、同じ読み替えを、航路の数だけ手で写していたからだ。写す先が一つしかなけりゃ、そもそも写し間違えようがない。

「一つ、正直に言っておきます」とルークが続けた。「読み替えは、前から『3以下』で正しかったんです。僕は、閾値を直したわけじゃありません。同じ『3以下』を、あちこちに写すのをやめて、一箇所に置き直しただけです。それと——この一箇所の通訳の中に、僕がもし間違った読み替えを書けば、その通訳は、やっぱり間違えます。誰も間違えなくなる、という魔法じゃないんです。消えるのは、あちこちに散った写しどうしが、食い違うこと。それだけです」。

正直な子だ、と思った。魔法で何もかも解決する、とは言わない。

「なるほどな。頭の中がずいぶんとスッキリしてきたぜ。だがルーク、この全体のつながりは、魔法の回路としてどう流れてるんだ?」 「それなら、この青写真を見てください」と、ルークは光り輝く青い羊皮紙を机の上に広げた。「僕たちの書いた『通訳』が、どう間を取り持っているかが一目でわかります」。

Adapter pattern diagram mapping WeatherSource interface and class relationships

「ほう……! 俺の『canFly』は、共通の約束である『WeatherSource』だけを見つめている。そして、その約束を果たすために、各観測所への通訳(アダプター)たちが、外部の観測所を懐に抱え込んで、言葉を翻訳しているわけか」 「はい」とルークは頷いた。「外部の観測所は僕たちの魔導書の外にあって、直接作り変えることはできません。ですが、こうしてアダプターが『委譲(デリゲーション)』の光線でつなぐことで、相手を書き換えることなく、僕たちの約束に従わせることができるんです」。 「変えられない相手はそのままに、間に立つ通訳がすべてを解決する。この絵を見ると、その意味が本当によくわかるな」と俺は感心した。

作り物の観測所を、結界に据えて

「本物の観測所に繋ぐ前に」とルークは言った。「結界の中へ、“作り物の観測所"を幾つか据えて、どれもちゃんと同じ形で読めるか、検めます」。魔術師は、本番の前に、安全な結界の中で魔導式を試すらしい。模擬詠唱、というそうだ。

まず、各通訳が、それぞれの形を、ちゃんと共通の読みに訳せるか。

1
2
3
4
5
// 各アダプタが固有の形を共通の読みへ正しく変換する(境界含む)
assertEquals(Condition.CLEAR, SkyEyeAdapter(SkyEyeStation("晴")).report().condition)
assertEquals(Condition.CLEAR, CloudGaugeAdapter(CloudGauge(1)).report().condition)
assertEquals(Condition.CLEAR, CloudGaugeAdapter(CloudGauge(3)).report().condition)   // 境界 3 は晴れ
assertEquals(Condition.CLOUDY, CloudGaugeAdapter(CloudGauge(4)).report().condition)  // 4 は曇り

assertEquals は、「訳した後の読みが、狙いどおりか」を照らす模擬詠唱だ。雲見の計の目盛りは、3までが晴れで、4からは曇り。その境目まで、きっちり検めてある。

次が、俺には面白かった。ルークは、“作り物の観測所"を使って、航路の判断そのものを試したんだ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 作り物の観測所(フェイク)。object は「中身が一つで足りるとき」の書き方(毎回同じ値を返すだけのシングルトン)
object FakeClear : WeatherSource {
    override fun report(): WeatherReport = WeatherReport(Condition.CLEAR)
}
object FakeStorm : WeatherSource {
    override fun report(): WeatherReport = WeatherReport(Condition.STORM)
}

assertEquals(true, canFly(FakeClear))    // 必ず晴れを返す観測所 → 出す
assertEquals(false, canFly(FakeStorm))   // 必ず嵐を返す観測所 → 見合わせ

「本物の観測所の中身がどうなっていようと、関係なく」とルーク。「こちらで用意した『必ず晴れを返す観測所』『必ず嵐を返す観測所』を渡すだけで、航路の判断が正しく動くかを検められます。判断が、外部の形から、ちゃんと切り離されている証です」。

そして、俺がいちばん気にしていた、絵符所のこと。ルークは、絵符所の通訳を、その場で一つ書いた。既にあるものには、指一本触れずに。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 絵符所(また別の形=絵符で返す観測所)。これも外部の仕掛けで、こちらでは作り変えられない
class PictoStation(private val glyphValue: Char = '☀') {
    fun glyph(): Char = glyphValue                          // '☀'=晴れ / '☁'=曇り / '⛈'=嵐
}

// 絵符所の通訳:絵符を、共通の読みへ訳す。足すのは、これ一つだけ
class PictoStationAdapter(private val station: PictoStation) : WeatherSource {
    override fun report(): WeatherReport = WeatherReport(
        when (station.glyph()) {
            '☀' -> Condition.CLEAR
            '☁' -> Condition.CLOUDY
            '⛈' -> Condition.STORM
            else -> Condition.UNKNOWN
        }
    )
}

この通訳を足すだけで、絵符所も、ちゃんと読める。

1
2
3
// 新しい観測所の通訳を「足すだけ」で canFly が動く(呼び出し側は一文字も変えない)
assertEquals(true, canFly(PictoStationAdapter(PictoStation('☀'))))
assertEquals(false, canFly(PictoStationAdapter(PictoStation('⛈'))))

絵符所の晴れの絵符は「出す」、嵐の絵符は「見合わせ」になった。北風路の判断も、南風路の判断も、既にある空目・雲見の通訳も、一文字も触っていない。ルークいわく、こういう「拡張のときは足すだけ、既にあるものには手を触れない」作りを、開放閉鎖(拡張には開いて、修正には閉じている、という設計の心得。頭文字で OCP とも)と呼ぶらしい。

最後に、観測所がちゃんとした報せを寄こせなかったとき。

1
2
3
4
// 異常系: 観測所が想定外の語を返す → UNKNOWN へ変換 → canFly は false(欠航扱い・例外は投げない)
val brokenStation = SkyEyeStation("???")
assertEquals(Condition.UNKNOWN, SkyEyeAdapter(brokenStation).report().condition)
assertEquals(false, canFly(SkyEyeAdapter(brokenStation)))

「観測できない、意味の取れない報せは、UNKNOWN に訳して」とルーク。「航路の判断は、それを見合わせ扱いにします。無理に出したりはしません。ここで、通訳が慌てて魔法を暴発させる——例外を投げる——ようなことも、しません。ただ静かに、見合わせにするだけです」。

俺は、この一連の検めを、自分の目でも辿ってみた。前の判断(航路ごとの読み替え)と、新しい判断(通訳ごしの canFly)で、同じ空をどう読むか。雲量1の日。快晴の日。雲量5の日。そして、意味の取れない報せの日。どれも、前と新しいので、寸分同じ答えになった。当たり前だ。ルークは、判断そのものは、何も変えていないんだから。変えたのは、読み替えを"どこに置くか”、それだけだ。

結界の中の模擬詠唱は、すべて、狙いどおりに通った。

変えられねえもんは、変えなくていい

書き直された運航判断を、俺は受け取った。

観測所ごとの読み替えは、航路の判断からきれいに消えていた。残ったのは、たった一つの共通の約束と、観測所ごとに一つずつ立った、小さな通訳だけ。

「……観測所は、一つも作り変えてねえんだな」と俺は言った。「あっちは、あっちの形のまま。俺が間に立ってやってた通訳を、こいつが一つずつ、肩代わりしてくれる」。

あの日、同じ空を逆に読んで欠航させた半日が、少し遠くなった気がした。相手を変えようと、こっちが背伸びしてたんだ。変えられねえもんは、変えなくてよかった。間に、一枚挟めば。

「これで、もう二度と、読み違えねえのか」。俺は念を押した。

ルークは、まっすぐ俺を見て、正直に答えた。「いえ。通訳の中身に、僕が間違った読み替えを書けば、その通訳は間違えます。ただ——読み替えは、もう一箇所にしかありません。あちこちの写しが食い違う、ということは、もう起きません。直すときも、その一箇所を直すだけです」。

「なるほどな」と俺は笑った。「取りこぼしが減る、って話か。魔法で誰も間違えなくなる、じゃなくてな」。

俺は、自分の手で、もう一つ確かめたくなった。「近く増える、絵符所だ。あれを繋ぐには、どこを直す?」。

ルークは、絵符所の通訳を一つ、さらりと書き足した。既にある canFly も、空目の通訳も、雲見の通訳も、指一本触れずに。「これだけです。航路の判断は、絵符所の"形"を、知らないままでいい」。

観測所が増えるたびに、航路ぜんぶを開いて、同じことを書いて回る。あの、うんざりする作業が、通訳一枚で済む。諦めていたものが、あっさり片づいたときの、あの妙な軽さがあった。

隅で、マグナス様が、写し終えた帳面を、ぽん、と閉じた。そして、ぽつりと言った。

「変えられぬものを、無理に変えることはない。間に立って、言葉を移す者が一人おれば——それで、二つは、手を取れるのだよ」。

帰り支度をしていると、マグナス様は、もう次の異国の札を手に取って、また帳面へ写し始めていた。ルークに向かって、何を教えるでもない。ただ、写す手だけが、静かに動いている。今日、あの人がルークに渡したのは、あの帳面の一枚と、問いを一つ。答えを口にしたところは、とうとう見なかった。あの子は、師匠に手を引かれてじゃなく、自分の足で、あの通訳役に辿り着いたんだ。

丘を下りて係留場へ戻った俺は、さっそく絵符所の通訳を一つ、実際の運航判断に繋いだ。航路の判断には、指一本、触れなかった。繋ぎ終えて、ふと、肩が軽いのに気づいた。

観測所の言葉が、みんな違っても、もういい。相手を変えなくていい。間に、一枚挟めば。俺の通訳は、もう、俺一人の肩には載っていない。

〔エピローグ〕師の手が、遠くなる

(ここからは、見習い魔術師ルークの手記だ。)

その日の夜、僕は、昼間のことを思い返していた。

師は、異国の文字を写す手を、ついに、一度も止めなかった。問いを一つ置いて、また写しに戻っただけだった。

背伸び期のころは、こうじゃなかった。僕が魔導式を暴走させるたび、師は水晶玉ごしに、一手ずつ、指図をくれた。ここをこう直せ、次はこうだ、と。それが、いつからだろう。近ごろの師は、問いを一つ、ぽつりと置くだけになった。今日も、答えは言わなかった。

師の手が、少しずつ、遠くなっている気がする。突き放されたのとは、違う。たぶん——待ってくれているのだ。僕が、自分で辿り着くのを。

思えば、前は、いつも師に問われて、初めて気づいた。「一番小さな道具で、足りるか」と、師の声で。

でも、今日は。継承の線が、外部の観測所の手前で止まって、繋がらなかったとき。師が何か言うより先に、僕は、自分自身に問うていた。「この相手は、そもそも、変えられるのか」と。

その問いは、いつのまにか、僕の中にあった。師の声を借りなくても、そこにあった。

まだ、うまくは言えない。今日わかったことだって、そう大したことじゃないのかもしれない。変えられないものを、無理に変えようとしない。ただ、それだけのことだ。

でも、それを、師の声を借りずに思えた日が、今日だった。師の手が遠くなるぶん、その問いは、これから、僕が自分で握っていよう。


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

  • 修復した呪文(クラス/モジュール名): WeatherSource(共通の約束)、WeatherReportCondition(共通の読み)、SkyEyeAdapterCloudGaugeAdapter(観測所ごとの通訳=Object Adapter)、canFly(共通の約束だけを見る運航判断)
  • 乱れの要因(アンチパターンの課題):
    • 外部の観測所が、提供元ごとに違う形(言葉・数字・絵符)で天候を返し、その読み替えを、呼び出し側(各航路の判断)が直に、しかも手写しで重複して持っていた。相手の形はこちらで変えられないのに、呼び出し側が相手の形を知りすぎていた(不整合インタフェースへの個別対応=密結合)。観測所が一つ増えるたび、全航路の判断を開いて書き足す羽目になり、書き忘れれば撥ねる。過去には手写しの一つに写し損じが紛れ込み、静かに一日を欠航にしたこともあった。
  • 過剰設計の反省(背伸びの過ち):
    • 「すべての観測所を一つの親から継承させて、形を揃える」と考えた。だが観測所は外部が所有する型で、こちらから親を継がせられない(既定で final・宣言に手を入れられない)。相手を作り変えようとした背伸びだった。師の「書き直せとは言えぬ、間に一枚挟むだけ」という問いから、「相手を変えず、間に通訳を立てる」に、自分で辿り着いた。
  • 魔術の定石(学んだ設計パターン):
    • Adapter(Object Adapter/委譲)。共通の約束 WeatherSource を決め、観測所ごとに「相手を持って、共通の読みへ訳す」通訳を一つずつ立てた。継承(相手になる)でなく委譲(相手を持って使う)なので、外部型が final でも効く。呼び出し側は共通の約束だけを見て、各観測所の形を知らない。新しい観測所は通訳を1つ足すだけで、呼び出し側は不変(開放閉鎖)。共通の口を真偽(canFly)でなく"読み”(report(): WeatherReport)にしたのは、真偽の差し替えだと別の定石(Strategy)と紛らわしく、Adapter の"形を訳す"本質がぼやけるから。パターンは、名前でなく意図で選ぶ。
  • 模擬詠唱の記録(検証のポイント):
    • 各通訳が固有の形→共通の読みへ正しく訳すこと(境界 <=3)、呼び出し側を"作り物の観測所”(フェイク)で外部非依存にテストできること、新しい観測所の通訳を足しても呼び出し側・既存テストが無改変で通ること(開放閉鎖)、観測不能を安全に見合わせ扱いにすること(異常系・例外を投げない)を確認。加えて、書き直した判断が、以前の判断と、代表的な空模様すべてで同じ答えを返すこと(=判定を変えていないこと)も照合した。
  • ちなみに:
    • Adapter が効くのは「呼び出し側が生の形に触れないから誤読せず、変換が一箇所で、一度書いて一度検められる」から。通訳の中身に誤った読み替えを書けば、その通訳は間違える(=誰も間違えなくなる魔法ではない)。消えるのは、散った写しどうしの食い違いだ。継承で書く Adapter(Class Adapter)もあるが、Kotlin はクラスの多重継承ができず、外部型は継承させられないことも多いので、委譲(Object Adapter)が既定になる。
  • 師匠マグナスの格言:
    • 「変えられぬものを、無理に変えることはない。間に立って、言葉を移す者が一人おれば——それで、二つは、手を取れるのだよ。」
comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。