Featured image of post コードナビゲーター【Template Method】港ごとに狂う寄港手順〜写し絵の星図と、一本の骨格〜

コードナビゲーター【Template Method】港ごとに狂う寄港手順〜写し絵の星図と、一本の骨格〜

港ごとにコピペされた寄港手続きが、規約改定の修正漏れで係留許可保留に。手順の骨格を親クラスへ一本化するTemplate Methodで「修正は一箇所」を実現する設計を、Rubyの継承とフックで解説します。

第1幕 異変——止まらせてもらえない船

アルタイル・ステーションは、氷粒の環の内側に浮いている。

舷窓の外を、砕いた硝子みたいな氷の粒がゆっくり流れていく。環の外周から港までは、決められた進入回廊を三段の減速で降りていく決まりだ。俺は機関室の操作席で、主機の逆噴射を一段目から順に当てていた。出力計の針が跳ねないように、燃焼室の温度が偏らないように、手と目と尻の下の振動で船を減速させていく。

名乗りが遅れた。俺はダグという。この恒星間貨物船〈オリオール号〉で機関を見て、二十年と少しになる。その二十年で身に染みたことが一つある。船ってのは、走らせるより止めるほうがずっと難しい。加速はしくじっても遅れるだけだが、減速をしくじった船は、止まるべき場所で止まらない。だから寄港進入は機関士の本番で、俺は今、その本番の真っ最中だった。

二段目の逆噴射を当て終えて、姿勢制御に切り替える。ここから先は管制交信だ。船の応答装置が港へ応答コードを送り、管制が係留許可を返す。いつもの手続き。いつもの手順。

返ってきたのは、許可じゃなかった。

「〈オリオール号〉、こちらアルタイル管制。応答コードに不備があります。減速性能値の申告がありません。係留許可を保留。現在の待機軌道で待機してください」

保留。二十年やっていて、そう何度も聞く言葉じゃない。

船は港を目の前にして、待機軌道に浮いた。主機は絞ってある。姿勢制御の小さな噴射だけで位置を保つ、機関士が一番嫌いな宙ぶらりんだ。機関ってのは、回っているか止まっているかのどちらかがいい。中途半端に温めたまま浮かんでいると、燃料は減るし、燃焼室には煤の癖がつくし、何より落ち着かない。

それに——俺は操作席で腕を組んだ——保留の理由が気に食わなかった。

減速性能値の申告がない、と管制は言った。減速性能値ってのは、この船がどれだけの質量をどれだけの距離で止められるかを示す数字で、機関室が計測して発行する。つまり、俺の数字だ。うちの機関が「ちゃんと止まれる」ことを示す数字が、管制に届いていない。機関士としてこれほど面白くない話はない。船は止まれる。止まれるのに、その証明が届いていないせいで、止まらせてもらえない。

待て。そういえば。

引っかかることがあった。前の寄港——カペラでも、その前のプロキオンでも、進入の少し前に手続きプログラムから機関室へ照会が飛んできた。「減速性能値を出せ」というやつだ。俺が端末に数字を返すと、手続きのほうでそれを応答コードに載せて送っていたらしい。それが今回は、来なかった。

来なかったのに、俺は気にしなかった。港が変われば流儀も変わる。アルタイルは採氷ステーションで、寄る船も少ない辺境の港だ。ここは古風なやり方なんだろう、くらいに流していた。

あの違和感は、これか。

俺は内線のスイッチを入れて、星図室を呼んだ。うちの航法士に、いま分かっていることだけを伝える。係留保留。理由は減速性能値の申告なし。それと——手続きからの照会が、この港に限って来ていないこと。

返事は短かった。

「そのまま浮いてて。降りる」

第2幕 解析——写し絵の間違い探し

ミラが機関室に降りてくるのは、珍しい。

うちの航法士は星図室の床に星図を広げて暮らしているような人で、機関室の油の匂いとは縁が薄い。その縁の薄い場所へ、ミラは片手に透明なフィルムの束を、もう片手に出力紙の筒を抱えて降りてきた。年齢不詳。歩き方はいつもどおり、廊下の継ぎ目を几帳面に踏まない変な足取りだ。付き合いが長いと、こっちの目も肥える。ミラが道具を抱えて動くときは、行き先の当たりがもうついている。

「ダグ、それ借りるね」

ミラが顎で指したのは、操作席の脇にある部品検査灯だった。板状の照明の上に部品を載せて、内部の亀裂を透かして見る検査台だ。ミラは断ってから、返事を待たずにフィルムの束を検査台に広げた。三枚ある。よく見ると、フィルムには細かい文字がびっしり焼き付けてあった。

コードだ。

「寄港手続きのプログラム。カペラ用、プロキオン用、アルタイル用。星図と同じで、コードも重ねると読めるものがあるの」

ミラは三枚のフィルムを、角を揃えてぴたりと重ね、検査灯の上に置いた。灯りがフィルム越しに透ける。星図の改版照合——版の違う星図を重ねて、動いた星を探す航法士の古典的なやり口だと、あとで教わった。それをコードでやっている。

覗き込んで、俺にも見えた。

ほとんどの行は、三枚がぴたりと重なって、太く濃い一本の線になっている。ところどころ、線が濃くならずにぼやけて滲む行がある。周波数の数字のあたり。検疫の行のあたり。三枚で中身が違う行は、像を結ばずに滲むわけだ。

そして、一枚だけおかしい場所があった。

途中の一行が、抜けている。二枚には線があるのに、一枚には無い。そして抜けた場所から下は、その一枚だけ全部の行が半行ずつ上にずれて、紙面の下半分が幽霊みたいな二重像になっていた。

「ここ。一枚だけ、行が一本抜けてる。抜けたところから下は、ぜんぶ半行ずれてる」ミラは滲んだ下半分を手のひらで示した。「抜けてるのはアルタイル用。抜けてる行は——管制交信の、減速性能値の併記」

出力紙の筒が広げられた。フィルムで見たものの実物、三枚の写しのうちの一枚。カペラ用の寄港手続きはこうなっていた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# カペラ港用の寄港手続き(プロキオン用・アルタイル用も同じ形の写し)
class CapellaDocking
  def dock(ship)
    log = []
    log << "減速: 三段減速(主機逆噴射→姿勢制御→微調整噴射)"
    log << "管制交信: 周波数128.6で呼出し、応答コード#{ship.registry}を送信"
    log << "管制交信: 減速性能値#{ship.braking_rating}を併記(管制規約第7版)"
    log << "係留: 誘導ビーコンに沿って進入し、係留クランプを接続"
    log << "検疫: 大気混入物検査を申請"
    log
  end
end

機関屋でも、コードくらいは読む。dock が寄港手続きの本体で、船の情報 ship を受け取って、実施する手順を上から順に記録に積んでいき、最後にその記録を返す。<< は Ruby の書き方で、配列(記録の束)の末尾に一件書き足す印だ。#{} は文字列の中に値を埋め込む印で、たとえば #{ship.registry} のところには船籍符号が入る。手続きはこの記録を管制へ送信し、照会が要る行——減速性能値のところ——では機関室に数字を出させる。俺の端末に飛んでくる、あの照会だ。

ちなみに船の情報のほうは、こういう小さな入れ物だ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 進入する船。船籍符号と減速性能値を持つ
class Ship
  attr_reader :registry, :braking_rating

  def initialize(registry:, braking_rating:)
    raise ArgumentError, "registry must not be empty" if registry.nil? || registry.empty?
    raise ArgumentError, "braking_rating must be positive: #{braking_rating}" unless braking_rating.positive?

    @registry = registry
    @braking_rating = braking_rating
  end
end

attr_reader は持ち物を外から読めるようにする Ruby の書き方、raise ArgumentError はおかしな値をその場で弾く門番。initialize の引数名の後ろに付いた : はキーワード引数といって、呼ぶ側が Ship.new(registry: "NGC-2244", braking_rating: 82) のように名前付きで渡す書き方だ。このへんは前にミラに教わった通りで、船籍が空だったり減速性能値がゼロ以下だったりする船は、そもそも入れ物を作った時点で弾かれる。

で、プロキオン用とアルタイル用はどうなっているのかと訊いたら、ミラは出力紙の残りを広げて見せてくれた。周波数の数字が違う。検疫の行が違う——カペラは大気混入物検査、プロキオンは生体貨物検査、アルタイルは持込水質検査で、氷の港のアルタイルだけは末尾に除氷点検が一行足されている。違いはそれだけだ。減速も、係留も、交信のやり方も、残りの行は三枚とも一字一句同じだった。

「写しなの」とミラは言った。「最初にカペラ用があって、プロキオンに寄ることになったとき、写して二枚目を作った。アルタイルで三枚目。それぞれの港で本当に違うのは、周波数と検疫だけ。でも写しは行ごと写すから、共通のところも三枚に増える」

「増えると、どうなる」

「共通のところを直す用事ができたとき、三回、同じ修正をすることになる」

ミラは検査灯の上の、あの半行ずれた一枚に目を落とした。

「管制規約の第7版。入港事故が続いて、管制が『この船はちゃんと止まれます』って証明を先に取る決まりになったの。応答コードに減速性能値を併記せよ——それが去年の改定。改定が来たとき、写しは三枚あった。カペラ用は直された。プロキオン用も直された。アルタイル用は」

「直され損ねた」

「うん。アルタイルは寄る回数が少ないから、模擬航走の当番表からも外れてた。誰も気づかないまま一年、今日まで来た」

俺は思わず言った。直したやつの不注意じゃないか、と。ミラは首を横に振った。

「直した人は二枚直してる。ちゃんと仕事してるの。三枚目を忘れたのは、三回同じ編集をやらせる形のほう。写しが三枚あれば、忘れる場所も三つある。四枚なら四つ。形が、忘れる場所を用意してる」

一つの改定に、三本の矢印。ミラの言った「形」ってやつを、俺は油圧系統の図面を引く要領で頭の中に描いてみた。

管制規約第7版の改定がカペラ用・プロキオン用・アルタイル用の3枚の写しへそれぞれ修正として送られるが、3回目の修正だけがアルタイル用に届かず、併記行なしのまま係留許可保留に至る流れを描いた星図風の図

それに、とミラは続けた。この写し方式を作った先人を悪く言うのも違う、と。

「これが書かれた頃、寄港先は三つで、規約の改定なんて年に一度あるかないかだった。それなら『その港のことは、その一枚を読めば全部分かる』写しのほうが、検分しやすくて誠実な作りなの。港と改定が増えた今の航路に、形が合わなくなっただけ」

ミラはフィルムを一枚、検査灯から持ち上げて、灯りに掲げた。先人の書いた手順の並びを、透かして検分するみたいに。それから、筒の中から最後の一枚の出力紙を出した。コードじゃない。試験だ。

「事故の再生をする。模擬航走で」

模擬航走——船を飛ばさず、シミュレータの上だけで手続きを走らせて、出来を確かめる試験航行だ。Ruby は動的型付けの言語で、間違いは書いた時点じゃなく動かした瞬間にしか鳴らない。だからこの試験が生命線になる、というのは前に教わって、俺の帳面の一番目立つところに書いてある。今回ミラが書いたのは、こういう一本だった。

1
2
3
4
5
6
7
8
9
# 規約整合の一括模擬航走: 全部の港の手続きに、第7版の併記行があるか
def test_regulation_compliance_across_all_ports
  docking_classes = [CapellaDocking, ProcyonDocking, AltairDocking]

  docking_classes.each do |docking_class|
    log = docking_class.new.dock(@ship)
    assert_includes log, "管制交信: 減速性能値82を併記(管制規約第7版)"
  end
end

三つの港の手続きを順ぐりに全部流して、記録の中に併記の行が入っているかを一括で照合する。each do |docking_class| ... end は、配列から一件ずつ取り出しては間の処理を繰り返す Ruby の書き方で、|〜| が取り出した一件につける名前だ。Ruby ではクラスそのものも値として配列に入れられるから、取り出した docking_class.new を呼べば、その港の手続きが作れる。@ship は試験用にあらかじめ作っておいた船で、減速性能値は82にしてある。assert_includes は Minitest(Ruby に標準で付いてくるテストの道具、これも前に教わった)の照合の一つで、「この一件を含んでいるはずだ」という答え合わせだ。

ミラがそれを、事故当時のまま保全してあるアルタイル用の写しに当てた。

警報。結果盤に赤が一つ。カペラは緑、プロキオンも緑、アルタイルだけが赤。併記の行が、記録のどこにも無い。

「事故はもう起きてるから、再現って言うのも変だけど」とミラは結果盤を見たまま言った。「警報は、こう鳴るはずだった。一年前に、この試験があれば」

それから応急の処置は早かった。ミラはアルタイル用の写しに併記の一行を書き足した。カペラ用とプロキオン用にはもう入っている、あの一行と同じものを、三枚目にも。試験をもう一度流す。全部緑。再交信。

「〈オリオール号〉、応答コードを受理しました。進入順17番。係留まで推定4時間」

船は列に並んだ。俺は操作席の背もたれに体を預けて、息を吐いた。めでたしめでたし——と言いたいところだが、腹の底に何か残っている。ミラも検査灯の上の三枚を片付けようとしない。俺は残っているものの正体を、口に出してみることにした。

「なあ。規約が変わるたび、同じ修正を、港の数だけ繰り返すのが俺たちの仕事か」

ミラがこっちを向いた。

「今回はたまたま三枚目で、たまたま係留保留で済んだ。だが次の改定でも、誰かがまた一枚忘れるんだろう。写しが四枚、五枚に増えてりゃなおさらだ。うちの寄港先は増える一方だぞ」

「そう。それが残ってる仕事」ミラは検査灯の三枚を、あらためて手のひらで示した。「忘れない人間を育てるのは、わたしの領分じゃないの。忘れる場所を消すのが、航法士の仕事」

「消せるのか」

「見て。三枚のうち、本当に三枚である必要がある線は、この滲んでるところだけ。濃い線は全部、一本でいい」

第3幕 引き直し——一本の骨格と、二種類の隙間

引き直すよ、とミラは言った。進入順17番、待ち時間は4時間ある。

「手順の並びを見て。減速、管制交信、係留、検疫。この並びは航路管理局の規約で決まってて、どの港でも同じ。港が勝手に順番を入れ替えることはできない。つまり並びは骨格なの。港ごとに違うのは、骨格じゃなくて、骨格の中の何箇所か——周波数と、検疫と、アルタイルの除氷。そこだけが隙間」

骨格は一本だけ書く。写しは全部やめる。ミラはそう言って、端末に新しいコードを起こし始めた。まず、骨格から。

 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
# 寄港手続きの骨格。手順の並びは航路管理局の規約で全港共通
class DockingProcedure
  def dock(ship)
    log = []
    log << decelerate
    log.concat(contact_control(ship))
    log << moor
    log << quarantine
    log.concat(extra_inspection)
    log
  end

  # --- 共通ステップ(全港同じ。ここが「一箇所」になる) ---
  def decelerate
    "減速: 三段減速(主機逆噴射→姿勢制御→微調整噴射)"
  end

  def contact_control(ship)
    [
      "管制交信: 周波数#{control_frequency}で呼出し、応答コード#{ship.registry}を送信",
      "管制交信: 減速性能値#{ship.braking_rating}を併記(管制規約第7版)"
    ]
  end

  def moor
    "係留: 誘導ビーコンに沿って進入し、係留クランプを接続"
  end

  # --- 必須の隙間(港ごとに必ず埋める) ---
  def control_frequency
    raise NotImplementedError, "#{self.class}が管制周波数を実装していない"
  end

  def quarantine
    raise NotImplementedError, "#{self.class}が検疫手順を実装していない"
  end

  # --- 任意の窓(開けたい港だけ開ける) ---
  def extra_inspection
    []
  end
end

concat は配列を配列ごと繋げる書き方だ。交信は二行あるから、一件ずつの << じゃなくてまとめて繋いでいる。raise の行にある self.class は「いま動いているものが属するクラスの名前」が入る書き方で、警報にどの港のクラスかを名乗らせるために使っている。

上から読んでいくと、dock はさっきの写しと同じ並びで手順を組み立てている。だが手順の中身をその場に書かず、decelerate だの quarantine だの、一つずつ別のメソッドに切り出して、名前で呼んでいる。減速と係留と交信には中身が書いてある。周波数と検疫には——中身の代わりに、妙なものが書いてあった。

「おい、この raise NotImplementedError ってのは何だ。門番の raise は知ってるが、実装していない、って自分で白状してるぞ」

「白状してるの。ここは骨格には書けません、港ごとに違います、って宣言。埋めるのは各港の担当」

そして各港の担当が、これだ。

 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
# カペラ港: 必須の隙間だけ埋める
class CapellaDocking < DockingProcedure
  def control_frequency
    "128.6"
  end

  def quarantine
    "検疫: 大気混入物検査を申請"
  end
end

# アルタイル港: 隙間に加えて、任意の窓も開ける
class AltairDocking < DockingProcedure
  def control_frequency
    "97.3"
  end

  def quarantine
    "検疫: 持込水質検査を申請"
  end

  def extra_inspection
    ["追加点検: 氷環通過後の除氷点検を実施"]
  end
end

プロキオン用も同じ形で、周波数が141.2、検疫が生体貨物検査になるだけらしい。

クラス名の後ろの < DockingProcedure が、今日の要だとミラは言った。< は「親を継ぐ」印。CapellaDockingDockingProcedure を親に持つ子で、親に書いてあるメソッドを、自分で書かなくてもそのまま使える。逆に、親と同じ名前のメソッドを自分で書けば、そっちが優先される。上書き——オーバーライドというそうだ。

俺は三つのクラスを見比べて、まず素直な疑問を口にした。

「待て。カペラ用にもアルタイル用にも、dock が無いぞ。手続きの本体はどこへ行った。これで CapellaDocking の手続きを呼んだら、何が動くんだ」

「そこが今日の一番大事なところ。Ruby はメソッドを呼ばれたら、まず自分のクラスを探すの。無ければ親を探す。それでも無ければ、そのまた親。メソッド探索っていうんだけど——カペラ用に dock は無いから、親の DockingProcedure まで遡って、骨格の dock が動く。で、骨格が途中で control_frequency を呼ぶと、今度はカペラ用に書いてあるから、そっちが動く。行ったり来たりして、一つの手続きが組み上がる」

「つまり、あれか」俺は検査灯の上の三枚のフィルムを見た。「写しが三枚あるんじゃなくて」

「原本が一冊あるだけ。カペラもプロキオンもアルタイルも、同じ一冊を読んでる」

腑に落ちる音がした気がした。写しってのは、写した瞬間から別々に育つ。だからズレる。原本が一冊なら、育ちようがない。ズレる場所が、最初から無い。

それは機関室にもある考え方だった。主機のオーバーホール手順書。あれは原本が一冊で、型式ごとに違うトルク値や規定圧は、原本の欄に銘板の値を差し込んで使う。型式の数だけ手順書を丸写しする工場は、少なくともまともな工場には無い。写せば版がズレる。それは機関屋なら体で知っている。

「で、港ごとの癖はどこへ行くんだ。骨格を親に上げたら、カペラの周波数だのアルタイルの除氷だのは、行き場がなくなるだろう」

「癖は消えないよ。住所が変わるだけ。よく見て、隙間には二種類あるの」

ミラは骨格のコードの、下半分を示した。

「周波数と検疫は、必ず埋める隙間。どの港にも必ずあるけど、中身は港ごとに違う。だから骨格側は空けておいて、埋め忘れたら NotImplementedError が鳴るようにしてある。除氷点検みたいな港の特別事情は、開けたい港だけ開ける窓。骨格側に『何もしない』っていう中身をあらかじめ入れておいて——extra_inspection が空っぽの配列を返してるでしょ、あれ——上書きしたい港だけが上書きする。フックっていうの。引っ掛けたい人だけが引っ掛ける、鉤」

必ず埋める隙間と、開けたい港だけ開ける窓。骨格は隙間と窓の位置と順番だけを知っていて、中身は知らない。中身は全部、港のクラスの持ち物だ。

俺は帳面を開いて、原本の一冊と三つの港の間柄を、係留索を引くみたいな線で描いてみた。

寄港手続きの骨格 DockingProcedure が手順の並びと共通ステップを一箇所だけ持ち、カペラ港・プロキオン港・アルタイル港の3つの子クラスが必ず埋める隙間(周波数と検疫)を埋め、アルタイル港だけが任意の窓(除氷点検のフック)も開けている継承構造を描いた星図風の図

そこまで聞いて、俺はもう一つ、逆側からの疑問をぶつけたくなった。機関屋の性分だ。

「待て、逆じゃ駄目なのか。港ごとの手続きをまるごと一個の部品にして、港に着くたび付け替える。うちの補機の交換みたいにさ。カペラに着いたらカペラの部品、アルタイルに着いたらアルタイルの部品。それなら親だの子だの言わなくて済むだろう」

「それも航法術としてある。やり方をまるごと部品に切り出して、動いてる最中に差し替えられるようにする定石——Strategy っていう名前がついてる」ミラはあっさり頷いた。「でも今回それを選ぶと、さっきの三枚に戻るの。まるごと一個の部品にするってことは、減速も係留も交信も、部品の中に全部書くってこと。港の数だけ部品を作れば、共通の手順が部品の数だけ、また写しになる」

「……振り出しか」

「そう。まるごと差し替えるのは、全体が入れ替わるときの型。今回は逆で、並びは固定、違うのは中の一部だけ。だったら、骨格は親が握って、隙間だけ差し替える。どっちが偉いって話じゃなくて、骨格を誰が持ってるか、差し替え口がどんな形か——そこが違う」

ミラはそこで一拍置いて、端末から顔を上げた。口調から、ふっと遊びが消える。技術の芯に触れるとき、この人は言葉を計器みたいに使う。

「この『手順の骨格を親に一本だけ書いて、変わるところだけ子に委ねる』航法術を、Template Method っていうの。Template Method——処理の骨格を親クラスのメソッドに定義して、可変のステップだけをサブクラス——さっきから子と呼んでいるもののことだ——の上書きに委ねる設計パターン。手順は、写すものじゃなくて、継ぐものよ」

継ぐ。継承——クラスが親の実装を受け継ぐ仕組みに、その字が当たっている理由が、今日は素直に飲み込めた。

ミラは検査灯の上のフィルムを、一枚、また一枚と剥がしていった。最後に一枚だけが検査台に残る。灯りに透けた手順の並びは、滲みのない、細く均された線に見えた。俺はさっきの線描きの余白に、こう書き付けた。原本は一冊。違いは記入欄へ。

「ただし」とミラが付け足した。「さっきの NotImplementedError、正直に言っておくことが二つある」

一つ。この例外は名前こそ「未実装」だが、Ruby では本来「この環境ではその機能が使えない」ことを知らせるための例外で、系統としては ScriptError の仲間になる。Ruby の例外には親子の系統があって、受け止める側の rescue——例外を捕まえて、船を止めずに処理を続けるための構文だ——は、何も指定しなければ StandardError の系統までしか捕まえない。つまりこの警報は、ありがちな「とりあえず全部 rescue で受け止めておく」書き方にも呑まれず、必ず表に出て試験を止める。抽象メソッド——さっきの「必ず埋める隙間」のように、親では中身を書かず子に埋めさせるメソッドのことだ——の表現としては、みんなが使う定番だが、意味としては少し借り物。定番の借用だと知って使うぶんには、実害より分かりやすさが勝つ。

二つ。こっちが大事だ。この警報は、その隙間が呼ばれた瞬間にしか鳴らない。

「埋め忘れたクラスを書いても、書いた時点では何も起きないの。定義は通る。誰かがその港に寄ろうとして、dock が走って、埋まってない隙間に踏み込んで、初めて鳴る」

Ruby の間違いは航行中にしか鳴らない——いつかの受け売りを、俺は思い出していた。だったら答えは一つしかない。飛ばす前に、全部の港へ飛ばしたことにしておく。つまり、模擬航走だ。

第4幕 模擬航走——二層の網と、係留クランプの音

進入順はまだ12番までしか進んでいない。ミラは模擬航走の束を、順に流し始めた。

最初の一群は、答え合わせというより約束の確認だった。応急処置を済ませた写し時代の三枚と、引き直した骨格版。同じ船の情報を両方に食わせて、返ってくる記録が一字一句同じであることを、三港ぶん照合する。全部緑。引き直しってのは、目的地を変えずに線の引き方を変えることだ——構造は変わった、挙動は変わっていない。それをまず数字で示してから、次へ行く。順番まで律儀だ。

次に、さっきの規約整合の一括模擬航走。三港の手続きを全部流して、第7版の併記行が全港の記録にあるかを一括照合するあの試験を、骨格版に当てる。全部緑。

「同じ試験だけど、意味が変わってるの」とミラは言った。「写し時代のこの試験は、三枚の写しが揃ってることを見張ってた。三枚が別々にズレうるから、見張る意味があった。いまは併記の行そのものが、親の contact_control の中に一箇所しかない。三港の記録に出てくる併記行は、全部その一箇所から来てる」

「見張る相手が、いなくなったわけか」

「うん。網は残すよ。でも、網にかかる種類の事故のほうが、構造ごと消えてる」

それから、埋め忘れの実演。ミラは検疫を書き忘れた架空の新港を、その場で書いてみせた。

1
2
3
4
5
6
# 検疫を埋め忘れた架空の新港(必須の隙間が一つ空いている)
class HalfBuiltDocking < DockingProcedure
  def control_frequency
    "99.9"
  end
end

定義した時点では、何も起きない。Ruby は文句ひとつ言わずにこのクラスを受け入れる。だが模擬航走でこの港に寄ろうとすると——dock が走り、減速が記録され、交信で周波数99.9が埋まり、係留が記録され、検疫の隙間に踏み込んだ瞬間、警報。NotImplementedErrorHalfBuiltDocking が検疫手順を実装していない。試験では assert_raises——「この例外が出るはずだ」という Minitest の答え合わせの一つで受け止める。

「書いた時点では鳴らない。呼んで初めて鳴る。だから新しい港のクラスを書いたら、就航の前に必ず模擬航走に一度寄らせる。隙間の埋め忘れは、それで全部拾える」

フックの検分もあった。カペラの記録に追加点検の行が無いこと。アルタイルの記録に除氷点検の行があること。窓は、開けた港でだけ開いている。門番の検分は前にやった型と同じだから軽く流す——船籍が空の船、減速性能値がゼロの船は、Ship を作った時点で ArgumentError に弾かれる。

結果盤は、全19本、41項目、全部緑だった。

「これで、次に規約第8版が来たら?」

「直すのは親の併記行、一箇所。直し忘れる残りの二枚は、もう存在しない。新しい港が増えたら、子のクラスを一枚書くだけ。埋めるべき隙間は骨格が知ってて、埋め忘れは模擬航走が鳴らす」

試験は事故を検出する網で、構造は事故を起こさせない骨格。層が二つある、と俺は帳面に書いた。網だけだと、かかるたびに誰かが直しに走る。骨格だけだと、埋め忘れに気づけない。両方あって、初めて枕を高くして飛べる。

ついでに、保証できないことも聞いておいた。うまい話には裏があると相場が決まっている。

「二つある」とミラは指を立てた。「一つ。Ruby には『この骨格メソッドは上書き禁止』って言語で縛る仕組みがないの。子のクラスが dock そのものを上書きしたら、骨格ごと迂回できちゃう。並びを勝手に変えた港の手続きが書けてしまう——そこを止めるのは言語じゃなくて、出航前点検と模擬航走の領分。二つ。骨格は隙間の位置を固定する。だから、検疫を二回やる港とか、係留の前に検疫する港とか、想定してない形の港が現れたら、無理に隙間へ押し込まないで骨格側を設計し直すこと。押し込み始めると、隙間だらけの骨格になって、しまいには写し時代に逆戻りする」

万能の航法術はない、というのはどの回でも変わらないらしい。骨格は「並びが本当に共通である」ことに賭けた設計で、その賭けが崩れたら賭け直す。それを最初から言っておくのが、うちの航法士の流儀ではある。

進入順が読み上げられるのを待つ間、ミラは検査台の縁に腰掛けて、妙なことを教えてくれた。この骨格の形、実は俺も毎日世話になっているのだという。Ruby の Enumerable——配列なんかの繰り返し機能の詰め合わせは、クラスが each(一件ずつ順に取り出す方法)を一つ実装するだけで、map(全件をまとめて変換する)も select(条件で絞り込む)も、揃いの機能をまとめて使えるようになる。厳密には親子の継承じゃなく、モジュールの混ぜ込みという別の仕掛けを使うらしいが、骨格は向こう持ちで、こっちは隙間を一つ埋めるだけ——形は今日のと同じだ。大小の比較を丸ごともらえる Comparable も同じ形。標準ライブラリってのは、この航法術で組んである場所だらけらしい。名のある定石ってのは、教科書の中じゃなくて、毎日握ってる工具の中にいる。

「〈オリオール号〉、進入順が来ました。回廊へ進入してください」

俺は操作席に戻って、絞ってあった主機を温め直した。三段減速の最後の一段。微調整噴射を細かく当てて、誘導ビーコンの光条に船首を乗せる。管制交信——今度は、一度で通った。

「応答コードを受理。ようこそアルタイルへ。桟橋7番へどうぞ」

氷粒の環が舷窓の下へ流れて、桟橋の係留アームが近づいてくる。姿勢を合わせ、相対速度をゼロに殺す。そして、船体に係留クランプの重い接続音が響いた。ガン、と一度、それから留め金の食い込む鈍い振動。何百回と聞いてきた音だが、今日のは格別に据わりがいい。止まるべき場所で、止まるべき手続きを踏んで、船が止まった音だ。

内線が鳴った。星図室からだった。

「ダグ、着いたね。乾パン食べる? 蜂蜜あるよ」

「遠慮しとく。俺はこれから機関の火を落とすんだ」

回線の向こうで、フィルムを片付ける乾いた音がしていた。俺は主機の停止手順に取りかかる。手順書は、原本が一冊。それでいい。


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

  • 航法術(パターン名): Template Method(処理の骨格を親クラスの1本のメソッドに定義し、可変ステップだけをサブクラスの上書きに委ねる設計)
  • 発生事象(症状): 管制規約第7版(減速性能値の併記義務化)がアルタイル用の写しにだけ未反映。寄港進入で交信不成立となり、係留許可保留のまま待機軌道へ
  • 原因(旧航路の問題): 寄港手続きが港の数だけコピーされ、共通行の修正が「3回の同じ編集」になっていた。写しが増えるほど忘れる場所も増える(コピー&ペーストによる重複=修正漏れドリフト)
  • 処置(引き直しの要点): 手順の骨格を親クラス DockingProcedure に一本化。港ごとの違いは「必ず埋める隙間」(抽象ステップ)と「開けたい港だけ開ける窓」(フック)へ住所を変えた。共通行の修正は以後一箇所で、直し忘れる残りの写しは存在しない
  • 保証外事項: 骨格メソッド自体の上書きは言語では禁止できない(Ruby に final 相当なし。出航前点検と模擬航走で守る)/骨格は手順の並びが全港共通であることに賭けた設計。想定外の並びの港が現れたら、隙間に押し込まず骨格側を設計し直す
  • 模擬航走結果: 全19テスト・41アサーション、全灯緑(写し版と骨格版の挙動一致・規約整合の一括照合・埋め忘れの NotImplementedError 検出・フックの既定と上書き・門番)
  • 次の針路: アルタイルで採氷の積み込みののち出航予定。規約第8版が来たら、直すのは骨格の一箇所
comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。