Featured image of post コードナビゲーター【Adapter】相手は変えられない〜港ごとの方言に通訳を立てる〜

コードナビゲーター【Adapter】相手は変えられない〜港ごとの方言に通訳を立てる〜

港ごとに通信規格が違う管制システムへの対応で、交信コード本体がcase分岐で方言を吸収し港が増えるたび本体を開ける旧設計を、Adapterパターンで港ごとの通訳に隔離。Rubyの委譲とダックタイピングの協調を解説します。

第1幕 異変——三港目の、また同じ作業

いい話から書いておく。〈オリオール号〉に、新しい寄港先が増えた。

アルビレオ・ステーション。新規契約の港で、初入港は三日後に迫っている。恒星間の貨物船にとって、寄れる港が増えるというのは荷主が増えるということだ。航海のたびに空荷の区画を眺めて唸る側からすれば、文句なしの朗報である。機関は快調そのもので、循環器の低い唸りも、主機の振動が床板を伝ってくる周期も、二十年聞き慣れた平常のそれだ。今回は、何も壊れていない。

壊れていないのに、俺の気が重いのはなぜか。話は交信卓から始まる。

寄港の数日前になると、船は港の管制へ入港要求を送る。事前ハンドシェイク試験——初めての相手と本番前に交信規格の疎通を確かめる、握手の儀式だ。応答が返れば、許可か保留か、許可なら泊地は何番か、が分かる。この交信を支える送受信機とアンテナ、それに交信卓のハード——そこから先が俺の受け持ちになる。出力調整は済ませた。アンテナの指向も合わせた。卓のハードは万全である。

問題は、卓の中身のほうだった。航法士側の作業予定表に、こう載っている。——アルビレオ対応: 交信コード本体の改修。

本体の改修。その一行を読んだ瞬間、過去二回の記憶が背中に乗ってきた。

一度目はデネブ港との契約のとき。二度目はヴェガ港が交信規格を改定したとき。どちらも交信コードの本体に手が入って、そのあとが長かった。ハンドシェイク試験は、新しい港の分だけでは済まなかったのだ。本体を開けた以上、触っていないはずの港の分まで疑うのが筋になる。ヴェガの応答は前と同じに読めているか。デネブの泊地番号は正しく拾えているか。全港ぶんの試し直しで、交信卓とアンテナ座の間を何往復したか、数える気にもならない。

配管なら分かる話なのだ。径の合わない配管が来たら、継手を挟む。本管を切る奴はいない。なんでコードのほうは、港がひとつ増えるたびに本管を切る話になるんだ——と、これは機関室のぼやきである。ぼやいたところで、卓の中身は航法士の領分だ。

星図室に上がると、うちの航法士は床にノートを広げていた。

ミラ。腕は船団一、行動は船団外。今日床に広がっているのは、この人が見習いの頃から付けているという手書きのノートだ。俺たちは勝手に「港の方言帳」と呼んでいる。寄港した港ごとに、交信規格の癖が採集してある。ヴェガのページには実際の応答文字列が切手みたいに切り貼りしてあって、余白に注記が入る。いわく——セミコロンで区切る。丁寧だが早口。デネブのページには——項目名で話す。几帳面。規格書の写しなら通信室にもあるが、こういう書き方はしない。この人は港の規格を、言葉のある相手として読む。港には港の方言がある、というのがミラの持論だ。

いまは膝の上にアルビレオの管制仕様書を広げて、方言帳に新しいページを起こしているところだった。

俺は作業予定表を掲げて、単刀直入にいった。

「三港目だぞ。港と話すたびに、船の頭を開けるのか?」

ミラの手が止まる。俺は続けた。ヴェガのとき、デネブのとき、本体を開けたあとの全港試し直しで誰が卓とアンテナ座を往復したか。それから、もうひとつ引っかかっていたことも言った。

「着艦の手順なら、前に引き直しただろう。手順の骨格を親に置いて、港ごとの差分だけ子に書くやつだ。今度のは、あれとは違うのか」

「違うの」とミラは顔も上げずに答えた。「あれは手順の話。うちの船の中の、降りる段取りをどう組むかの話だった。今度のは、言葉の話。相手の返事の形が港ごとに違って、うちの船はまだアルビレオの言葉を知らない」

そして顔を上げた。

「うん。だから今回は、開けないやり方にする」

「開けずに済むなら、二回目のときに言ってくれ」

「二回目のときはまだ、三回目が来るって確信がなかったの」と、ミラは方言帳を閉じもせずに言った。どう開けずに済ませるのかは、言わなかった。この人が仕組みの説明を後回しにするときは、先に見せたいものがあるときだ。

第2幕 解析——変えられないものから、逆に読む

星図室の投影に、交信コードの本体が上がった。いま実機で動いている、現行版だ。

 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
# 交信卓の入港要求。港ごとの応答形式の違いを、本体の case が吸収している。
class DockingConsole
  def hail(port_id, ship_id)
    case port_id
    when :vega
      reply = VegaControl.new.transmit_query(ship_id)
      status, detail = reply.split(";")
      if status == "GO"
        log_grant(ship_id, detail.sub("BAY-", ""))
      else
        log_hold(ship_id, detail)
      end
    when :deneb
      reply = DenebControl.new.negotiate(ship_id)
      if reply["clearance"] == "granted"
        log_grant(ship_id, reply["bay"])
      else
        log_hold(ship_id, reply["reason"])
      end
    else
      raise ArgumentError, "未対応の港: #{port_id}"
    end
  end

  # log_grant / log_hold は記帳(航海記録の文字列生成)。港と無関係の共通処理。
  def log_grant(ship_id, berth)
    "入港要求: #{ship_id} → 接近許可(泊地#{berth})"
  end

  def log_hold(ship_id, reason)
    "入港要求: #{ship_id} → 接近保留(#{reason})"
  end
end

骨格は読める。hail は呼びかけ、の意で、港の識別子と船体符号を受け取って入港要求を送り、応答を航海記録の一行にして返すメソッドだ。case port_id は港の識別子で枝分かれする分岐で、:vega の頭のコロンはシンボル——名前そのものを軽い値として使う書き方。どちらもプローブの一件で見たばかりの型である。分岐のどれにも入らなければ、else の門番が「未対応の港」と警報を上げる。

初見の書き方も混ざっていたので、そこはミラに止めてもらいながら読んだ。

ヴェガの枝の reply.split(";") は、文字列を区切り文字で切り分けて配列にするメソッドだ。ヴェガの管制はセミコロン区切りの文字列で応答してくる。許可なら "GO;BAY-07"、保留なら "HOLD;CONGESTION"。これを split(";") に通すと ["GO", "BAY-07"] という二枚に分かれる。左辺の status, detail = は多重代入といって、配列の中身を先頭から順に、並べた変数へ割り当てる書き方だ。一枚目が status に、二枚目が detail に入る。その次の行の detail.sub("BAY-", "") は、文字列の最初の一致箇所を置き換えるメソッドで、"BAY-07" から "BAY-" を剥がして "07" だけ取り出している。泊地番号の抽出だ。

デネブの枝は様子が違う。デネブの管制は Hash——名前と値の対応表——で応答してくる。許可なら { "clearance" => "granted", "bay" => "12" }、保留なら { "clearance" => "denied", "reason" => "magnetic-storm" }。だから枝の中身も reply["clearance"] と項目名で聞きにいく形になる。

同じ「入港要求への応答」なのに、ヴェガはセミコロンの文字列で話し、デネブは項目名の表で話す。方言帳の注記そのままだ。

「で、これを書いた人の話をしておくね」

ミラは投影の脇に、古い改修記録を二枚並べた。左は、この船がまだヴェガとしか取引していなかった頃の交信コード。case すらない。ヴェガの応答を split して読む処理が、本体にそのまま素直に書いてある。右は、デネブ契約のときの改修差分。case が生まれて、デネブの枝が一本増えている。

「一港目のこれは、いちばん読みやすい書き方なの。相手がヴェガしかいないなら、ヴェガの言葉で直に書くのが最短で、誰が読んでも迷わない。二港目のこの差分も、正しい。港が二つになった日に、分岐をひとつ足す。橋を一本まるごと架け直すより、ずっと安い。どっちの時点でも、これ以上うまいやり方は、たぶんなかった」

じゃあ何が悪かったんだ、と訊きかけて、やめた。ミラが先に言った。

「悪かった人は、いないの。三港目が来る未来だけが、当時は見えてなかった。それだけ」

それから、ミラは俺のほうへ向き直った。ここからが本題、という顔だった。

「ダグ、今日はいつもと逆から読むね。先に、動かせないものを置く」

ミラは指を三本立てて、一本ずつ折っていった。

一つ。港の管制モジュールは、各港の当局が配布してくる外部の部品だ。ヴェガの VegaControl も、デネブの DenebControl も、届いたアルビレオの AlbireoControl も、中身はよその組織のものであって、うちの船では書き換えられない。「ヴェガに『セミコロンやめて項目名で話して』って頼む航路は、ないの」。

二つ。港は増え続ける。新規契約は商売の生命線で、止める理由がない。つまり「今回だけ我慢して開ける」は成立しない。四港目も五港目も来る。

三つ。船内の各システム——航海計画も、入港に合わせて主機を温める機関側の暖機予約も——が交信結果から読みたいものは、突き詰めれば三つしかない。許可か否か。許可なら泊地はどこか。駄目なら理由は何か。それだけだ。セミコロンも項目名も、船の中の誰も欲しがっていない。

「動かせない壁を先に引くとね、残ってる自由は、実はひとつしかないの。港ごとの言葉の違いを、どこで吸収するか。いまは本体の case の中——船の頭の中で吸収してる。それを、動かせる」

ミラは現行コードの中の何箇所かを、投影のマーカーで光らせた。"GO""BAY-""granted""reason"

「これ、全部よその港の言葉なの。船の頭の中に、他所の方言が住んでる。ヴェガが規格を変えた日のこと、覚えてるでしょ。あの日うちが開けたのは、うちの船の本体だった。相手の都合で、自分の頭を開ける関係になってるの」

覚えている。あの改修も、そのあとの全港試し直しも。相手の都合で自分の頭を開ける、という言い方は、あの徒労の据わりの悪さを言い当てていた。

「で、アルビレオの言葉を、いまの本体にそのまま聞かせるとどうなるか。見せておくね」

模擬航走の出番だ。実船をアルビレオに近づける前に、シミュレータ上でコードだけを試験航行させる——うちの星図室では何をやるにもこれが先に来る。ミラはまず、現行の本体に :albireo を渡す試験を書いて流した。結果は分かりきっている。else の門番が鳴って、「未対応の港: albireo」。本体は、教わっていない港とは話せない。ここまでは健全な赤だ。

「もっと怖いほうも見せておく。改修を急いだ誰かが、デネブの枝を写してアルビレオの枝を作ったとするね。アルビレオの応答は配列——順番に並んだ二枚——で返ってくるんだけど、写した枝はデネブのつもりだから、項目名で聞きにいく」

配列に向かって、Hash の流儀で項目名を聞く。流すと、結果盤に見慣れない警報が上がった。

NoMethodError。そのメソッドを持たない相手に呼びかけたときの、Ruby の実行時エラーだ。

「Ruby の間違いは、航行中にしか鳴らない。それは前からずっと言ってる通り。この NoMethodError もそうで、書いた時点では何も鳴らないの。コードの上では、配列に項目名で聞く行は、書けてしまう。鳴るのは、アルビレオと実際に交信した瞬間」

「形の合わない継手と同じだ」と俺は言った。「締めたときには漏れない。水を流した瞬間に漏れる」

「そう。だから網は、港ごとの模擬交信で張る。……それは After でも変わらないから、あとでちゃんと言うね」

ミラは床の方言帳を閉じた。そして閉じた表紙の上に、手のひらを置いた。開いて調べながら話すのをやめた、ということだ。ここから先は、暗記でしゃべる範囲——つまり、この人の頭の中ではもう引き終わっている範囲、ということでもある。

「ダグ。相手は変えられない。だったら、船が期待する受け答えを、船が先に決めるの」

第3幕 引き直し——通訳を、境界に立てる

引き直しの最初の一手は、コードではなかった。

ミラは投影を白紙に切り替えて、三行だけ書いた。

1
2
3
request_docking(ship_id)
  → 許可: { granted: true,  berth: "泊地番号" }
  → 保留: { granted: false, reason: "理由" }

「これが、船の言葉。今日からうちの交信卓は、この形しか聞かない」

request_docking——入港を頼む、という船側の動詞。船体符号を渡すと、シンボルをキーにした Hash がひとつ返る。書き方もひとつ新顔で、granted: true はデネブの応答で見た "clearance" => "granted" と同じ Hash の対を、:granted => true と書く代わりに省略した記法だ。キーがシンボルのときだけ、こう縮められる。granted が真なら泊地番号 berth が付く。偽なら理由 reason が付く。この対もセットで規格のうちだ、とミラは念を押した。真のときに理由はなく、偽のときに泊地はない。読む側が迷わないように、返りの形まで決めておく。

順序が逆なのだ、と気づくのに少しかかった。いままでうちの船は、港の言葉を覚えにいっていた。ヴェガが来ればヴェガの言葉を本体に書き、デネブが来ればデネブの言葉を書き足した。相手に合わせて、こちらの頭が形を変えてきた。いま目の前で起きたのはその逆で、まだ誰とも話していないうちに、うちの船の聞きたい形が先に決まった。これからは港の言葉のほうが、この形に訳されて船に入ってくる。

「で、訳すのは誰だ」

「港ごとに、一人ずつ立てるの」

ミラは投影に、新しいクラスを上げた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# ヴェガ港の通訳。ヴェガの方言を、船の共通規格に訳すことだけが仕事。
class VegaAdapter
  def initialize(control = VegaControl.new)
    @control = control
  end

  def request_docking(ship_id)
    status, detail = @control.transmit_query(ship_id).split(";")
    if status == "GO"
      { granted: true, berth: detail.sub("BAY-", "") }
    else
      { granted: false, reason: detail }
    end
  end
end

「既にあるものの受け答えの形を、使う側が期待する形に変換する部品を挟む。この航法術を Adapter っていうの。変換アダプタの、あのアダプタ」

そして、こっちを見て付け足した。

「相手は変えられない。変えられるのは、受け取り方だけ」

コードを読む。VegaAdapterinitialize——インスタンスを作るときに走る支度のメソッド——で、ヴェガの管制モジュールを @control に抱え込む。頭の @ はインスタンス変数、つまりそのオブジェクトが自分の持ち物として抱え続ける変数の印だ。そして request_docking と呼ばれたら、抱えている相手に transmit_query と、ヴェガの言葉で頼む。返ってきたセミコロン区切りを split して、"GO" かどうかを見て、さっき決めた船の言葉——grantedberth、あるいは grantedreason の Hash——に組み直して返す。

この「相手を抱えて、仕事をそちらに頼み、結果を整えて返す」形を、委譲と呼ぶのだそうだ。寄港手続きの一件で使った継承——親の性質を受け継いで子になる、いわば「である」の関係——とは別物で、委譲は相手を持ち物として抱える「を持つ」の関係になる。通訳はヴェガ管制の子孫ではない。ヴェガ管制を鞄に入れて連れて歩く、別の一人だ。俺の言葉に直すなら、継手は配管の子孫じゃない。配管と配管の、間の部品だ。

見どころは、と言われて画面に顔を寄せた。"GO" も、"BAY-" も、セミコロンも——ヴェガの方言のすべてが、この一枚の中に閉じ込められている。この部品の外には、もう一文字も漏れていない。

initialize(control = VegaControl.new)= VegaControl.new は引数の既定値というやつで、引数を省略して VegaAdapter.new と作れば本物のヴェガ管制を抱え、試験のときだけ別の相手を渡して差し替えられる。実運用では素で使い、模擬航走では取り回す、両にらみの書き方だ。

デネブの通訳は、テンポよく進んだ。型は一枚目で見たとおりで、方言だけが違う。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# デネブ港の通訳。
class DenebAdapter
  def initialize(control = DenebControl.new)
    @control = control
  end

  def request_docking(ship_id)
    reply = @control.negotiate(ship_id)
    if reply["clearance"] == "granted"
      { granted: true, berth: reply["bay"] }
    else
      { granted: false, reason: reply["reason"] }
    end
  end
end

項目名で話すデネブの流儀は、この一枚の中だけの事情になった。判定の形——「合言葉が一致すれば許可、それ以外は何が来ても保留側に倒す」——は、元の本体の枝とわざと同じに組んである、とミラは言った。引き直しは挙動を変えない。想定外の応答が保留側に落ちる癖まで含めて、倒れ方ごと引き継ぐ。

そして、本体だ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 交信卓の本体。港の名前も方言も、もう知らない。
class DockingConsole
  def initialize(adapter)
    @adapter = adapter
  end

  def hail(ship_id)
    reply = @adapter.request_docking(ship_id)
    if reply[:granted]
      log_grant(ship_id, reply[:berth])
    else
      log_hold(ship_id, reply[:reason])
    end
  end

  # log_grant / log_hold は記帳(航海記録の文字列生成)。Before と完全一致。
  def log_grant(ship_id, berth)
    "入港要求: #{ship_id} → 接近許可(泊地#{berth})"
  end

  def log_hold(ship_id, reason)
    "入港要求: #{ship_id} → 接近保留(#{reason})"
  end
end

case が、丸ごと消えている。

本体は通訳を一人受け取って抱え、hail と言われたら通訳に request_docking と頼み、返ってきた船の言葉の granted を見て記帳するだけ。引き方にも注目してほしい。港の応答は文字列キーだから reply["clearance"] と引いたが、船の言葉はシンボルキーだから reply[:granted] と引く——キーに使った値の種類と、取り出すときの書き方は、対で決まる。元のコードと並べて目で確かめたが、:vega:deneb も、"GO""granted" も、港の名前と方言はどこにも残っていない。記帳の二つのメソッドは、一文字も動いていない。

頭の中で、配管図を引き直すみたいに前後を並べてみた。方言がどこに住んでいるか——それだけ追えば、今日の引き直しの正体は一枚で見える。

BeforeとAfterの構造対比図。Beforeは交信卓本体DockingConsoleのcaseがヴェガのセミコロン区切り文字列とデネブの文字列キーHashという港ごとの方言を直接解読し、それぞれの港の言葉で交信している。Afterは本体が船の共通規格request_dockingにだけ話しかけ、VegaAdapter・DenebAdapterという港ごとの通訳が委譲で各港の管制モジュールに頼み、方言は各通訳の中だけに閉じ込められることを示す

ひとつ、呼び方が変わっていることには気づいた。前は hail(:vega, ship_id) と、どの港かを毎回引数で渡していた。いまは hail(ship_id) だけだ。港はどこで決まるのか。

「作るとき。DockingConsole.new(VegaAdapter.new) って、通訳を渡して組み立てるの。港を選ぶ瞬間が、交信のたびから、寄港計画の一度きりに動いたの」

寄港計画がヴェガ行きなら、ヴェガの通訳を持たせた卓を組む。それだけらしい。どの通訳をどう選んで渡すかの仕掛けの話は、「選び方は今日の話じゃないの」と一言で流された。今日の主役は、選ぶことではなく訳すことだ、と。

その言い方が、逆に引っかかった。

「待て。発進管のときと同じ話じゃないのか。窓口を一つ作って、そこに寄せるんだろう」

プローブの一件が頭にあった。あのときも「増えるたびに本体を開ける」のをやめるために、組み立てを専用の窓口に出した。今日のこれと、何が違う。

「寄せる場所を作る、っていう手つきは似てる。でもあっちは、生まれの話なの。どの機体を作るかは、うちの船の中で決めていいことだった。決めていいことを、決める場所ごと一箇所に集めた」

「……作るものは、こっちの都合で決められたな。確かに」

「こっちは、国境の話。港の言葉は港が決める。うちでは決められない。決められることを窓口に集めるのがあっちで、決められないものを境界で受け止めるのが、今日のこれ」

決められることを集める。決められないものを受け止める。手つきが似ていても、向いている方角が逆だ。

もうひとつ、似た絵を知っていた。

「なら、部品を港の数だけ並べるんだろう。航路の計画ごとに方針の部品を差し替えた、あれとは何が違う。あれも部品を取っ替え引っ替えする話だったぞ」

「見分け方はひとつだけ。部品を別のに差し替えたら、意味が変わる?」

ミラは方言帳の表紙に手を置いたまま続けた。運航方針の部品は、燃料優先に差し替えても最短時間に差し替えても、どれも正しい。どれを選ぶかが仕事の部品——選ぶ部品だ。今日の通訳はどうか。ヴェガの卓にデネブの通訳を差したら、それはただの誤訳だ。港ごとに、正しい訳はひとつしかない。選択の余地があるのではなく、対応が決まっている——訳す部品。

「選ぶ部品と、訳す部品。形が同じでも、仕事が違うの」

形とは委譲のことだ。抱えて、頼んで、返す。その骨格だけ見れば同じでも、選ぶために並ぶのか、訳すために立つのかで、別の航法術になる。棚の形が同じでも載っているものが違えば別物だ、と言われたプローブの一件と、勘定の仕方は同じだった。

得心しかけたところで、実務の側から確かめておくべきことが残っていた。

「交信卓は、渡された相手が本物の通訳かどうか、どうやって確かめるんだ。ヴェガの通訳です、って名札でも見るのか」

「確かめない。request_docking に応えてくれるなら、誰でもいい」

ダックタイピング——同じ名前の呼びかけに応えられれば、型は問わない。Ruby の流儀として、航路計算機の一件で最初に教わった考え方だ。今日の通訳たちは、共通の親も、共通の何かの宣言も持っていない。ただ全員が request_docking に応える、それだけで揃っている。他の言語なら interface という書類——「この受け答えができます」という宣言文——を書いて判子をつく場面だが、Ruby に書類はない。船の共通規格は、書類ではなく約束としてだけ存在する。

「代償も言っておくね。約束が書類にならないってことは、約束破りを事前に見つける仕組みも、ないの。通訳を噛ませ忘れて、生の港モジュールを直接卓に渡しても、組んだ時点では何も鳴らない。鳴るのは交信の瞬間、さっき見た NoMethodError。——だから、約束が書類にならない言語では、模擬航走が書類の代わりなの」

もうひとつ、これは俺の性分で聞いた。気の利く奴が通訳に入ると、ろくなことにならない。

「通訳が気を利かせて、駄目な応答を握り潰したらどうなる。保留を三回まで隠して再送する、みたいな親切をやり始めたら」

「通訳が交渉を始めたら、外交事故」

即答だった。通訳の仕事は訳すことだけ。保留は保留のまま、理由ごと訳して通す。再送するか、待つか、別の港に回るか——判断は本体と、その先の航海計画の仕事だ。再試行も、応答のもっともらしさの審査も、記帳も、通訳には持たせない。持たせていいのは、訳の正しさだけ。

「訳が薄いほど、通訳は信用できるの」

そこで話が一段落して、星図室が静かになった。ミラはヘッドセットを首に掛けたまま、床に戻した方言帳へアルビレオの続きを書き足しはじめる。仕様書と見比べて、応答の形式を新しいページに写している。俺は卓の裏の配線を留め直しながら、その静かな作業を横目で見ていた。書き終わる頃には、この港の癖もあの帳面の住人になるんだろう。

手が空いたところで、俺は一度機関室に降りた。戻ったとき、手には工具箱から抜いてきた物がひとつ。異径継手。径の違う配管同士をつなぐ、何の変哲もない変換部品だ。それを交信卓の上に、ことりと置いた。

「つまり、これだろ。径の違う配管が来るたびに本管を切って溶接し直すのは、馬鹿のやることだ。間にこれを一個噛ませとけば、次に径が変わっても、換えるのはこれだけで済む」

ミラは何も言わなかった。ただ、書きかけのアルビレオのページの隅に、いま卓に載っている継手の絵を、ちいさく描き足した。

第4幕 模擬航走——三港の網と、先に届く声

模擬航走の本番は、約束の確認から始まった。

引き直しは挙動を変えない——その証明が先頭に来る。ヴェガとデネブ、許可と保留、計四通りの交信を、現行の case 版と通訳を立てた版の両方に流し、記帳が一字残らず一致することを照合する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def test_vega_grant_identical
  assert_equal(
    @before_console.hail(:vega, "SHIP-001"),
    @after_vega_console.hail("SHIP-001")
  )
end

def test_vega_hold_identical
  assert_equal(
    @before_console.hail(:vega, HOLD_SHIP_ID),
    @after_vega_console.hail(HOLD_SHIP_ID)
  )
end

@before_console たちは、試験の支度の場所で組んである。case 版はそのまま、通訳版は After::DockingConsole.new(After::VegaAdapter.new) と、通訳を渡して組む——さっきの「港を選ぶ瞬間は組み立てのとき」が、試験の形にそのまま出ている。頭の Before::After:: は、同名クラスの二つの版が試験の中でぶつからないように分けてある住所書きで、これもいつもの段取りだ。なお、港の配布モジュールには試験用の取り決めがあって、決まった符号(HOLD_SHIP_ID)で呼ぶと保留、それ以外の符号には決まった内容の許可を返してくる。応答が毎回同じだから、記帳の照合が成り立つわけだ。保留側の記帳まで含めて、四通り、全部一致。緑。

次が、今日の主役の網だ。通訳の単体照合——港ごとの方言が、船の言葉に正しく訳されているかを、通訳一人ずつに確かめる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def test_vega_adapter_translates_grant
  assert_equal(
    { granted: true, berth: "07" },
    After::VegaAdapter.new.request_docking("SHIP-001")
  )
end

def test_deneb_adapter_translates_hold
  assert_equal(
    { granted: false, reason: "magnetic-storm" },
    After::DenebAdapter.new.request_docking(HOLD_SHIP_ID)
  )
end

「誤訳はね、ここでしか捕まらないの」とミラは言った。通訳を立てる設計は、訳の正しさを構造では守ってくれない。"GO" を見るべきところで "OK" を見る通訳は、書けてしまうし、組めてしまう。捕まえるのは、港ごとのこの照合だけだ。「通訳を立てた船は、通訳の試験を港の数だけ持つ。それが飼い賃」。

三本目は、ちょっとした余興に見えて、そうではない。試験の中でだけ使う、最小の偽通訳だ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class StubAdapter
  def request_docking(_ship_id)
    { granted: true, berth: "99" }
  end
end

def test_console_accepts_any_object_that_responds_to_request_docking
  console = After::DockingConsole.new(StubAdapter.new)
  assert_equal(
    "入港要求: SHIP-999 → 接近許可(泊地99)",
    console.hail("SHIP-999")
  )
end

StubAdapter はどの港のモジュールも抱えていない。ただ request_docking と呼ばれたら決まった返事をする、それだけの人形だ。それでも卓は、何の文句もなく記帳まで走り切る。渡された相手が誰かを、本体は本当に見ていない——約束に応えるなら誰でもいい、の実演である。引数名の頭の _ は「受け取るが使わない」ことを示す Ruby の書き癖だ。

四本目は、約束破りの記録。通訳を噛ませ忘れて、届いたばかりの生のアルビレオ管制を卓に直結する。

1
2
3
4
def test_console_raises_no_method_error_when_given_raw_control
  console = After::DockingConsole.new(AlbireoControl.new)
  assert_raises(NoMethodError) { console.hail("SHIP-003") }
end

AlbireoControlhandshake には応えるが、request_docking という呼びかけは知らない。卓が頼んだ瞬間、NoMethodError。assert_raises は「この警報が鳴るはず」という照合だ。後ろの波括弧はブロック——その場では実行せず、相手に渡して実行してもらう処理の塊——で、assert_raises はこのブロックを走らせて、その最中に指定の警報が鳴ることを確かめる。鳴れば、この試験は緑になる。壊れ方を、直すためではなく、忘れないために試験簿に綴じておく——うちの模擬航走の流儀だ。組んだ時点では鳴らず、交信の瞬間に鳴る、という性質ごと記録されている。

そして、仕上げ。アルビレオ対応、本番である。

ミラが書いたのは、新しいファイル一枚だった。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# アルビレオ港(新港)の通訳。配列の応答を船の言葉に訳す、それだけの部品。
class AlbireoAdapter
  def initialize(control = AlbireoControl.new)
    @control = control
  end

  def request_docking(ship_id)
    status, detail = @control.handshake(ship_id)
    if status == "cleared"
      { granted: true, berth: detail }
    else
      { granted: false, reason: detail }
    end
  end
end

アルビレオの管制は順番で話す。応答は二要素の配列で、許可なら ["cleared", "03"]、保留なら ["standby", "traffic"]。一枚目が判定、二枚目が中身。だから通訳は handshake の返りを多重代入で受けて、船の言葉に組み直す。方言帳の新しいページに書いてあった癖——順番で話す。せっかち——が、そのまま十数行になった格好だ。

これを模擬航走に掛ける。アルビレオの通訳を渡した卓で、許可——通る。保留——通る。

1
2
3
4
5
6
7
def test_new_port_grant_log
  console = After::DockingConsole.new(After::AlbireoAdapter.new)
  assert_equal(
    "入港要求: SHIP-004 → 接近許可(泊地03)",
    console.hail("SHIP-004")
  )
end

ここで、朝からの勘定を締めておく。アルビレオ対応で書いたのは、通訳一枚。開いたファイルも、通訳一枚。交信卓の本体は、開いてすらいない。試験の構成そのものがその証明になっていて、この試験が読み込んでいる卓のソースは、ヴェガとデネブの検分に使ったものと同一のファイルだ。そして本体を触っていないから、ヴェガとデネブの試験は当然そのまま緑——触っていない港を疑う理由が、構造ごと消えている。

今日書き足したものと、指一本触れていないものを、勘定書きの形にしておく。アルビレオの声が船に届くまでの道筋は、こうだ。

新港追加時の変更範囲の図。新港アルビレオの外部管制モジュールの配列応答を、新しく書く通訳AlbireoAdapter一枚が船の共通規格に訳して渡し、交信卓本体DockingConsoleは一行も触らず記帳の接近許可(泊地03)まで流れる。既存のVegaAdapter・DenebAdapterには触らないことを点線で示す

前の二回、俺が卓とアンテナ座を何往復もした理由は、本体を開けたからだった。今回は、開けていない。だから、往復がない。徒労の正体は作業量じゃなくて、影響の読めなさのほうだったんだと、なくなってみて分かる。

検分は全部で十九本、赤はゼロ。ミラは結果盤を端から確かめてから、言った。

「効能の言い方、正確にしておくね。港を足しても本体を開けない、は結果の半分。もう半分はね——相手の都合が、船の中に入ってこなくなったの。ヴェガが明日規格を変えても、開くのはヴェガの通訳一枚。デネブにもアルビレオにも、本体にも、指一本届かない。影響の半径が、部品一枚ぶんに縮んだの。壊れなくなった、じゃないのよ。壊れる場所が、境界より内側に入れなくなったの」

守ってくれないものも、いつも通り確かめた。ミラは三つ挙げた。

一つ。港の規格変更そのものは、防げない。ヴェガが方言を変えれば、ヴェガの通訳は直す。この設計が約束するのは「直す場所が一枚に決まっている」ことまでで、「直さなくていい」ではない。

二つ。訳の正しさは、構造が守らない。さっき言った通り、誤訳を捕まえるのは港ごとの模擬交信だけだ。港が増えるたび、通訳の試験も増やす。網の手入れをやめた船から、誤訳は入ってくる。

三つ。港がひとつしかない船には、過剰だ。一港目のあの素直な直書きは、あれはあれで一番読みやすい形だった。通訳を雇う勘定が合うのは、言葉の違う相手が増え続けると決まった船だけ。うちは、そうなった。だから引いた。

数日後——アルビレオまで残り三日の航行中に、本番のハンドシェイクが組まれた。

実機の交信卓は、もう通訳版に載せ替えてある。俺は送信出力の計器の前、ミラは卓の端末の前。船体符号を載せた入港要求が、三日先の港へ飛んでいく。応答待ちの数秒、交信卓のスピーカーがかすかに砂の音を立てる。

音が、言葉になった。

「オリオール号、応答受理。泊地03を予約した。三日後に会おう」

一度で、通った。初めて聞くアルビレオ管制の声は、思ったより若かった。卓の記帳には、接近許可(泊地03)の一行。本体のコードは、今日まで一度も開けていない。

ミラは方言帳のアルビレオのページを、書き上がった切手と注記ごと、ぱたんと閉じた。

「いい声。この港の方言、好きよ」

俺は卓の上に置きっぱなしだった異径継手を工具箱に戻しながら、妙な気分を味わっていた。船はまだ着いていないのに、言葉のほうが先に通じている。三日後に初めて会う港と、うちの船はもう握手を済ませた。四港目の契約が来ても、次に俺がやることは決まっている。方言帳のページが一枚増えて、通訳が一人増える。卓の中身の心配は、もうしなくていい。


🚀 オリオール号 航海日誌(Navigator’s Log)

  • 航法術(パターン名): Adapter(既存のオブジェクトの受け答えの形を、利用側が期待する形に変換する部品を挟む構造パターン。今回は委譲——相手を抱えて頼み、結果を整えて返す形——で実装)
  • 発生事象(症状): 新規契約港アルビレオへの初寄港を前に、交信コード本体の改修が三たび作業予定に載った。港ごとの応答規格の違いを本体の case が吸収しており、港が増えるたび本体を開け、触っていない港まで含めた全港ぶんの試し直しが発生していた
  • 原因(旧航路の問題): 外部インターフェース差異の本体への漏出。書き換えられない相手(各港当局の配布モジュール)の方言が本体の分岐にむき出しで並び、相手の都合(規格変更・港の追加)がそのまま船内の改修になる。方言の取り違えは書いた時点では鳴らず、交信の瞬間の NoMethodError としてしか現れない
  • 処置(引き直しの要点): 船側が期待する共通規格(request_docking → granted / berth / reason)を先に定め、港ごとに方言をこの形へ訳す通訳(Adapter)を委譲で実装。本体から港の名前と方言が消え、港の追加は通訳一枚の追加で完結。ダックタイピングにより本体は「約束に応える相手なら誰でも」受け入れ、相手の都合の影響半径が通訳一枚に局所化された
  • 保証外事項: 港の規格変更そのものは防げない(直す場所が一枚に決まるだけ)/訳の正しさは構造が守らない(港ごとの模擬交信が唯一の網。港が増えれば通訳の試験も増やす)/通訳に翻訳以外の仕事(再送・審査・記帳)を持たせない規律は人が守る/相手がひとつで増えない船には過剰(直書きが読みやすい場面もある)
  • 模擬航走結果: 全19テスト・20アサーション、全灯緑(2港×許可保留の Before=After 記帳一致・3港の通訳単体翻訳照合・偽通訳によるダックタイピングの実演・通訳なし直結の NoMethodError 記録・新港の本体無改変追加)
  • 次の針路: 三日後、アルビレオ入港。方言帳には新しいページが増え、交信卓の中身は、もう増えない
comments powered by Disqus
システム開発・AIワークフローのご相談は Meetsource
Hugo で構築されています。
テーマ StackJimmy によって設計されています。