火曜日の昼下がり。俺はペットクリニックのバックヤード——いわゆる事務スペースで、デスクに張り付いてコードを睨んでいた。
モニタには PawsHeart::Clinic のソースコードが映っている。ref($animal) eq 'PawsHeart::Animal::Dog' という行が画面の至るところに散らばっていて、スクロールするたびに同じパターンが現れる。犬。猫。鳥。犬。猫。鳥。
俺の名前は速水慧介。30歳。エンジニア歴5年。地元のペットクリニックチェーン「ぱうずハート動物病院」のシステムを、新卒2年目からたった一人で構築してきた。予約管理、診察レポート生成、予防接種スケジュール計算、食事指導レポート——全部、俺が書いた。犬・猫・鳥の3種に完全対応。3年間、一度も落ちていない。院長の信頼も厚い。
……はずだった。
「速水さん、そろそろ爬虫類と小動物も診られるようにしてもらえませんか? 近所のエキゾチック専門病院が閉まったんですよ」
院長のその一言が、1週間前のことだ。
「もちろんです」と即答した。簡単だと思った。動物クラスを一つ足して、各処理に分岐を一つ追加するだけだ——そう思った。
だが、コードを開いた瞬間に血の気が引いた。generate_report()、calc_vaccine_schedule()、generate_diet_plan()——3つの操作関数すべてに ref() による分岐が並んでいる。爬虫類を足すなら、この3つ全部に分岐を追加しないといけない。さらに今後、小動物を足すなら、また3つ全部。エキゾチックバードなら——
「……多すぎる」
追い打ちをかけたのは先月配属された後輩だ。「速水さん、このコード……読めないです」。正直カチンときた。3年間動いてきたコードだぞ。
でも、確かに。俺自身も、どこを直せばいいか分からなくなりかけている。
院長に「ちょっと時間がかかります」と伝えたら、「知り合いにコードを診てくれる人がいるから」と言われた。コードを診る? 医者じゃないんだから——と思ったが、断る理由もなかった。
往診
ドアがノックされた。
「はい、どうぞ」
開いたドアの向こうに、二人組が立っていた。一人は黒いジャケットを着た男。無表情で、目が鋭い。もう一人は柔らかい雰囲気で、白いブラウスの上に淡い色のカーディガンを羽織っている。
俺は一瞬、動物病院の業者だと思った。
「先生の方ですか? 診察室はこっちの——」
「いえ、診るのはコードのほうです」
もう一人のほうが微笑みながら言った。「大丈夫ですよ、ここはコード診療所です……あ、いえ、往診ですね。コードを診させていただきに参りました。助手のナナコと申します。こちらがコードドクターの先生です」
コードドクター。院長が言ってた「知り合い」というのは、こういうことか。
黒ジャケットの男——先生は、俺のデスクには目もくれず、壁に貼ってある動物種別対応表をじっと見つめていた。犬・猫・鳥の3列。各列に対応する診察項目がびっしり書き込まれたホワイトボード。
「まあ、一応見てもらいますけど」俺は椅子の背にもたれた。「正直、ちゃんと動いてるんですよ。3年間落ちてないし」
先生が対応表を指差した。
「……3列か」
それだけ言って、俺のモニタの前に座った。勝手に。
触診
先生の指がトラックパッドを動かし、コードをスクロールし始めた。PawsHeart::Clinic のソース。画面を一目見た瞬間、先生の眉がわずかに動いた——ように見えた。何を考えているのか、まったく読めない。
先生は3つの操作関数を行き来した。generate_report()、calc_vaccine_schedule()、generate_diet_plan()。それぞれの中にある ref() の行を、指でトン、トン、トンと画面越しに示す。
| |
「……同じ傷が、3箇所」
ナナコさんがすかさず通訳した。「同じ型チェックの分岐が3つの関数に散らばっているんです。いわば、全身に同じ炎症が出ている状態ですね」
「いや、動物ごとに処理が違うんだから、分岐は当然だろ」
俺は反論した。当たり前じゃないか。犬と猫と鳥で診察の仕方が違うんだから、型を見て処理を分けるのは正しい判断だ。
ナナコさんが少し首を傾げた。
「動物の種類ごとに診察のやり方を変えるのは当然ですよね。でも……新しい動物が来るたびに、全部の診察マニュアルを書き換えるお医者さん、大変じゃないですか?」
——。
俺は言葉に詰まった。まさに今、爬虫類を追加しようとして、3つの関数全部を修正しなきゃいけない状況に直面している。俺はまさに「全部の診察マニュアルを書き換えようとしている医者」だった。
先生が ref() の行を指差して、一言。
「多発性分岐硬化症」
ナナコさんが補足する。「分岐が硬くなって、新しい種類を受け入れられなくなっているんです。今は3種類だから動いていますが、4種類目を入れようとすると全身に炎症が広がる——構造的な疾患ですね。治療法はありますよ」
診断
先生がモニタの横に、一枚のメモを置いた。走り書きで、たった二行。
| |
「Visitor……デザインパターンの?」
先生は答えない。代わりにナナコさんが説明してくれた。
「Visitorパターンは、データ構造に触れずに新しい操作を追加できる設計手法です。今の速水さんのコードは、新しい動物種を追加するたびに全操作関数を修正していますよね。Visitorを使えば、動物クラスと操作ロジックを分離できるんです」
「分離?」
「はい。各動物に『自分を紹介する窓口』を設けて、操作のほうを独立したクラスにするんです。新しい動物が来たら動物クラスを1つ追加するだけ。新しい操作が必要になったらVisitorクラスを1つ追加するだけ。既存のコードには触りません」
既存のコードに触らない。それは夢みたいな話に聞こえた。でも——デザインパターンの教科書でVisitorの名前だけは見たことがある。「使いどころが難しい」と書いてあった気がする。
「……本当にそうなるなら、やってほしい」
先生がかすかに頷いた——ように見えた。
外科手術
先生がキーボードに手を伸ばした。俺のコードに、直接触る気だ。一瞬、身構えた。3年間育ててきたコードだ。他人に触られるのは正直、気持ちのいいものじゃない。
だが先生の指は迷いなく動き始めた。
受容体の設置
まず先生が手をつけたのは、動物クラスだった。PawsHeart::Animal::Dog を開き、数行だけ書き足す。
| |
同じコードを Cat、Bird にも追加していく。
「……受容体」
ナナコさんが通訳する。「それぞれの動物に『自分を紹介する窓口』を作るんです。受付カードみたいなものですよ。犬は『犬です』、猫は『猫です』と自分で名乗る。お医者さん——つまりVisitorの側が、型を判定する必要がなくなるんです」
「受付カード……ダブルディスパッチってやつか?」
先生がこちらをちらりと見た。反応があったのは初めてだった。何も言わなかったが、否定もしなかった。合っているらしい。
切除と再構築
次に先生は新しいファイルを作った。PawsHeart::Visitor::ReportVisitor。
| |
俺は画面を凝視した。generate_report() の中にあった if/elsif 分岐が——丸ごと消えている。あったはずのロジックが、visit_dog、visit_cat、visit_bird という個別のメソッドに分離されていた。
「あ、if文が……消えた」
「……切除完了」
ナナコさんが微笑んだ。「診察マニュアルを1冊の分厚い本から、動物ごとのカードに分けたんですよ。犬のカードには犬の診察方法だけ。猫のカードには猫の診察方法だけ。新しい動物が来たら、カードを1枚足すだけです」
先生は同じ要領で VaccineVisitor と DietVisitor も作った。3つの巨大な操作関数が、3つのスリムなVisitorクラスに生まれ変わった。
拡張テスト
そして先生は——何の予告もなく—— PawsHeart::Animal::Reptile を作った。
| |
爬虫類クラス。たったこれだけ。そして既存のVisitor3つに、それぞれ visit_reptile メソッドを追加した。
| |
「……完了」
「え、これだけ? 俺が1週間悩んだのが——」
先生が俺の肩の方に手を伸ばした。
——え。なんだ。褒めてくれるのか? いや、慰め? 俺の1週間の苦労を認めてくれるのか?
先生の手は俺の肩を通り過ぎて、デスクの端にあるコーヒーメーカーのボタンを押した。
「……」
ナナコさんが棚からカップを取り出して差し出した。「先生、お砂糖は?」
「ブラック」
「……え?」
俺の存在、完全に忘れてるだろ。
術後経過
コーヒーを飲みながら(俺にもくれた。ナナコさんが気を利かせてくれたらしい)、先生が書いたテストの結果を確認した。
| |
テストを実行した。
| |
全部グリーン。21テスト、全パス。
爬虫類がいる。テストの中に、爬虫類がちゃんといる。既存の犬猫鳥のテストは1つも壊れていない。
ナナコさんが壁の対応表を指差した。「速水さん、あの対応表——今は3列ですよね」
視線を壁に向けた。犬。猫。鳥。3列。
「4列目を足すのも、こんなに簡単なのか……」
「そうです。そして、もし将来『体重測定レポート』のような新しい操作を追加したくなっても、新しいVisitorクラスを1つ作るだけです。既存の動物クラスには一行も触りません」
先生が帰り支度を始めた。カップを静かに置き、鞄を手に取る。
「……コミットメント」
ナナコさんが通訳する。「先生は治療費の代わりに、テストをきちんと書くことをお願いしているんです。これからも動物が増えたら、テストを書いて確認してくださいね」
「テストか……ああ、俺、テスト書いてなかったな。accept のたびにちゃんとテスト追加するようにする」
先生がドアに向かった。振り返らず、一言。
「感謝は、このコードに」
ナナコさんが最後に壁の対応表を指差して微笑んだ。「4列目、楽しみにしてますね」
二人が出ていった後、事務スペースに一人残された。静かだった。診察室の向こうで犬が「ワン」と鳴いた。
俺はホワイトボードマーカーを手に取り、対応表の4列目に書き入れた。
爬虫類
「……悔しいけど、すげえ医者だった」
処方箋まとめ
| 症状 | 適用すべき | 経過観察 |
|---|---|---|
ref() や isa で型を判定する if/elsif 分岐が複数の関数に散在している | ✓ | |
| 新しいデータ型を追加するたびに、既存の全操作関数を修正する必要がある | ✓ | |
| 同じ型チェックパターンが複数の関数にコピペされている | ✓ | |
| データ構造は安定しているが、操作の追加が頻繁に発生する | ✓ | |
| データ型が1〜2種類で、今後増える見込みがない | ✓ | |
| 操作が1種類しかなく、単純なポリモーフィズムで十分対応できる | ✓ |
治療のステップ
- 受容体を設置する(accept メソッド): 各データクラスに
accept($visitor)メソッドを追加し、$visitor->visit_xxx($self)でダブルディスパッチを実装する - 操作を独立させる(Visitor クラスの作成): 既存の操作関数から型チェック分岐を抽出し、Visitorクラスとして独立させる。各型に対応する
visit_xxxメソッドを実装する - 既存の分岐を切除する: 元の操作関数の if/elsif 連鎖を削除し、
$animal->accept($visitor)の一行に置き換える - 拡張性を検証する: 新しいデータ型(動物種)を追加した際に、既存コードの修正が不要であることをテストで確認する
- 新操作の追加手順を確立する: 新Visitorクラスの追加だけで新操作が使えることを確認し、チームの開発規約に組み込む
助手より
速水さん、今日はお疲れさまでした。「3年間ちゃんと動いてきた」——その言葉に、速水さんのエンジニアとしての誠実さがにじんでいました。動いているコードを否定されるのは辛いことですよね。でも、先生の治療は「動かないコードを直す」のではなく、「動いているコードを、もっと長く健康に動かし続ける」ためのものなんです。4列目の爬虫類、そしていつか5列目、6列目が加わっても、速水さんのシステムは柔軟に受け入れてくれるはずですよ。ぱうずハート動物病院の動物たちのために、これからも素敵なコードを書いてくださいね。
——ナナコ
