Featured image of post コードドクター【Visitor】多発性分岐硬化症〜全身に散らばったref()を切除せよ〜

コードドクター【Visitor】多発性分岐硬化症〜全身に散らばったref()を切除せよ〜

火曜日の昼下がり。俺はペットクリニックのバックヤード——いわゆる事務スペースで、デスクに張り付いてコードを睨んでいた。

モニタには 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() の行を、指でトン、トン、トンと画面越しに示す。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
sub generate_report ($self, $animal) {
    my $ref = ref($animal);

    if ($ref eq 'PawsHeart::Animal::Dog') {
        return sprintf("【診察レポート】犬: %s (%s)\n体重: %skg\n所見: ...",
            $animal->name, $animal->breed, $animal->weight);
    }
    elsif ($ref eq 'PawsHeart::Animal::Cat') {
        my $indoor = $animal->is_indoor ? '室内飼い' : '外飼い';
        return sprintf("【診察レポート】猫: %s (%s)\n飼育形態: %s\n所見: ...",
            $animal->name, $animal->breed, $indoor);
    }
    elsif ($ref eq 'PawsHeart::Animal::Bird') {
        # ... 鳥用の処理
    }
    else {
        die "未対応の動物です: $ref";
    }
}

「……同じ傷が、3箇所」

ナナコさんがすかさず通訳した。「同じ型チェックの分岐が3つの関数に散らばっているんです。いわば、全身に同じ炎症が出ている状態ですね」

「いや、動物ごとに処理が違うんだから、分岐は当然だろ」

俺は反論した。当たり前じゃないか。犬と猫と鳥で診察の仕方が違うんだから、型を見て処理を分けるのは正しい判断だ。

ナナコさんが少し首を傾げた。

「動物の種類ごとに診察のやり方を変えるのは当然ですよね。でも……新しい動物が来るたびに、全部の診察マニュアルを書き換えるお医者さん、大変じゃないですか?」

——。

俺は言葉に詰まった。まさに今、爬虫類を追加しようとして、3つの関数全部を修正しなきゃいけない状況に直面している。俺はまさに「全部の診察マニュアルを書き換えようとしている医者」だった。

先生が ref() の行を指差して、一言。

「多発性分岐硬化症」

ナナコさんが補足する。「分岐が硬くなって、新しい種類を受け入れられなくなっているんです。今は3種類だから動いていますが、4種類目を入れようとすると全身に炎症が広がる——構造的な疾患ですね。治療法はありますよ」


診断

先生がモニタの横に、一枚のメモを置いた。走り書きで、たった二行。

1
2
病名: 多発性分岐硬化症
処方: Visitor

「Visitor……デザインパターンの?」

先生は答えない。代わりにナナコさんが説明してくれた。

「Visitorパターンは、データ構造に触れずに新しい操作を追加できる設計手法です。今の速水さんのコードは、新しい動物種を追加するたびに全操作関数を修正していますよね。Visitorを使えば、動物クラスと操作ロジックを分離できるんです」

「分離?」

「はい。各動物に『自分を紹介する窓口』を設けて、操作のほうを独立したクラスにするんです。新しい動物が来たら動物クラスを1つ追加するだけ。新しい操作が必要になったらVisitorクラスを1つ追加するだけ。既存のコードには触りません」

既存のコードに触らない。それは夢みたいな話に聞こえた。でも——デザインパターンの教科書でVisitorの名前だけは見たことがある。「使いどころが難しい」と書いてあった気がする。

「……本当にそうなるなら、やってほしい」

先生がかすかに頷いた——ように見えた。


外科手術

先生がキーボードに手を伸ばした。俺のコードに、直接触る気だ。一瞬、身構えた。3年間育ててきたコードだ。他人に触られるのは正直、気持ちのいいものじゃない。

だが先生の指は迷いなく動き始めた。

受容体の設置

まず先生が手をつけたのは、動物クラスだった。PawsHeart::Animal::Dog を開き、数行だけ書き足す。

1
2
3
sub accept ($self, $visitor) {
    $visitor->visit_dog($self);
}

同じコードを CatBird にも追加していく。

「……受容体」

ナナコさんが通訳する。「それぞれの動物に『自分を紹介する窓口』を作るんです。受付カードみたいなものですよ。犬は『犬です』、猫は『猫です』と自分で名乗る。お医者さん——つまりVisitorの側が、型を判定する必要がなくなるんです」

「受付カード……ダブルディスパッチってやつか?」

先生がこちらをちらりと見た。反応があったのは初めてだった。何も言わなかったが、否定もしなかった。合っているらしい。

切除と再構築

次に先生は新しいファイルを作った。PawsHeart::Visitor::ReportVisitor

 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
package PawsHeart::Visitor::ReportVisitor;
use v5.36;

sub new ($class) {
    return bless {}, $class;
}

sub visit_dog ($self, $dog) {
    return sprintf("【診察レポート】犬: %s (%s)\n体重: %skg\n所見: 一般的な犬の健康診断を実施。",
        $dog->name, $dog->breed, $dog->weight);
}

sub visit_cat ($self, $cat) {
    my $indoor = $cat->is_indoor ? '室内飼い' : '外飼い';
    return sprintf("【診察レポート】猫: %s (%s)\n飼育形態: %s\n所見: 猫特有の腎臓・泌尿器チェックを実施。",
        $cat->name, $cat->breed, $indoor);
}

sub visit_bird ($self, $bird) {
    my $fly = $bird->can_fly ? '飛行可能' : '飛行不可';
    return sprintf("【診察レポート】鳥: %s (%s)\n飛行能力: %s\n所見: 羽毛と嘴の状態を確認。",
        $bird->name, $bird->species, $fly);
}

1;

俺は画面を凝視した。generate_report() の中にあった if/elsif 分岐が——丸ごと消えている。あったはずのロジックが、visit_dogvisit_catvisit_bird という個別のメソッドに分離されていた。

「あ、if文が……消えた」

「……切除完了」

ナナコさんが微笑んだ。「診察マニュアルを1冊の分厚い本から、動物ごとのカードに分けたんですよ。犬のカードには犬の診察方法だけ。猫のカードには猫の診察方法だけ。新しい動物が来たら、カードを1枚足すだけです」

先生は同じ要領で VaccineVisitorDietVisitor も作った。3つの巨大な操作関数が、3つのスリムなVisitorクラスに生まれ変わった。

拡張テスト

そして先生は——何の予告もなく—— PawsHeart::Animal::Reptile を作った。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package PawsHeart::Animal::Reptile;
use v5.36;

sub new ($class, %args) {
    return bless {
        name        => $args{name}        // 'Unknown',
        species     => $args{species}     // 'Unknown',
        temperature => $args{temperature} // 25,
    }, $class;
}

sub name        ($self) { $self->{name} }
sub species     ($self) { $self->{species} }
sub temperature ($self) { $self->{temperature} }

sub accept ($self, $visitor) {
    $visitor->visit_reptile($self);
}

1;

爬虫類クラス。たったこれだけ。そして既存のVisitor3つに、それぞれ visit_reptile メソッドを追加した。

1
2
3
4
5
# ReportVisitor に追加
sub visit_reptile ($self, $reptile) {
    return sprintf("【診察レポート】爬虫類: %s (%s)\n適正温度: %s℃\n所見: 鱗の状態と脱皮周期を確認。",
        $reptile->name, $reptile->species, $reptile->temperature);
}

「……完了」

「え、これだけ? 俺が1週間悩んだのが——」

先生が俺の肩の方に手を伸ばした。

——え。なんだ。褒めてくれるのか? いや、慰め? 俺の1週間の苦労を認めてくれるのか?

先生の手は俺の肩を通り過ぎて、デスクの端にあるコーヒーメーカーのボタンを押した。

「……」

ナナコさんが棚からカップを取り出して差し出した。「先生、お砂糖は?」

「ブラック」

「……え?」

俺の存在、完全に忘れてるだろ。


術後経過

コーヒーを飲みながら(俺にもくれた。ナナコさんが気を利かせてくれたらしい)、先生が書いたテストの結果を確認した。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
my $dog = PawsHeart::Animal::Dog->new(
    name => 'ポチ', breed => '柴犬', weight => 10
);
my $reptile = PawsHeart::Animal::Reptile->new(
    name => 'カメ吉', species => 'ヒョウモントカゲモドキ', temperature => 28
);

my $reporter = PawsHeart::Visitor::ReportVisitor->new;

# acceptで自分を紹介し、Visitorが適切なメソッドを呼ぶ
my $dog_report     = $dog->accept($reporter);
my $reptile_report = $reptile->accept($reporter);

テストを実行した。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
ok 1 - 犬の診察レポート
ok 2 - 犬種が含まれる
ok 3 - 猫の診察レポート
ok 4 - 室内飼いが反映される
ok 5 - 鳥の診察レポート
ok 6 - 飛行能力が反映される
ok 7 - 爬虫類の診察レポート(新種追加)
ok 8 - 適正温度が反映される
...
ok 19 - 爬虫類の適正温度が食事指導に含まれる
ok 20 - 新Visitorで犬をチェック
ok 21 - 新Visitorで鳥をチェック
1..21

全部グリーン。21テスト、全パス。

爬虫類がいる。テストの中に、爬虫類がちゃんといる。既存の犬猫鳥のテストは1つも壊れていない。

ナナコさんが壁の対応表を指差した。「速水さん、あの対応表——今は3列ですよね」

視線を壁に向けた。犬。猫。鳥。3列。

「4列目を足すのも、こんなに簡単なのか……」

「そうです。そして、もし将来『体重測定レポート』のような新しい操作を追加したくなっても、新しいVisitorクラスを1つ作るだけです。既存の動物クラスには一行も触りません」

先生が帰り支度を始めた。カップを静かに置き、鞄を手に取る。

「……コミットメント」

ナナコさんが通訳する。「先生は治療費の代わりに、テストをきちんと書くことをお願いしているんです。これからも動物が増えたら、テストを書いて確認してくださいね」

「テストか……ああ、俺、テスト書いてなかったな。accept のたびにちゃんとテスト追加するようにする」

先生がドアに向かった。振り返らず、一言。

「感謝は、このコードに」

ナナコさんが最後に壁の対応表を指差して微笑んだ。「4列目、楽しみにしてますね」

二人が出ていった後、事務スペースに一人残された。静かだった。診察室の向こうで犬が「ワン」と鳴いた。

俺はホワイトボードマーカーを手に取り、対応表の4列目に書き入れた。

爬虫類

「……悔しいけど、すげえ医者だった」


処方箋まとめ

症状適用すべき経過観察
ref()isa で型を判定する if/elsif 分岐が複数の関数に散在している
新しいデータ型を追加するたびに、既存の全操作関数を修正する必要がある
同じ型チェックパターンが複数の関数にコピペされている
データ構造は安定しているが、操作の追加が頻繁に発生する
データ型が1〜2種類で、今後増える見込みがない
操作が1種類しかなく、単純なポリモーフィズムで十分対応できる

治療のステップ

  1. 受容体を設置する(accept メソッド): 各データクラスに accept($visitor) メソッドを追加し、$visitor->visit_xxx($self) でダブルディスパッチを実装する
  2. 操作を独立させる(Visitor クラスの作成): 既存の操作関数から型チェック分岐を抽出し、Visitorクラスとして独立させる。各型に対応する visit_xxx メソッドを実装する
  3. 既存の分岐を切除する: 元の操作関数の if/elsif 連鎖を削除し、$animal->accept($visitor) の一行に置き換える
  4. 拡張性を検証する: 新しいデータ型(動物種)を追加した際に、既存コードの修正が不要であることをテストで確認する
  5. 新操作の追加手順を確立する: 新Visitorクラスの追加だけで新操作が使えることを確認し、チームの開発規約に組み込む

助手より

速水さん、今日はお疲れさまでした。「3年間ちゃんと動いてきた」——その言葉に、速水さんのエンジニアとしての誠実さがにじんでいました。動いているコードを否定されるのは辛いことですよね。でも、先生の治療は「動かないコードを直す」のではなく、「動いているコードを、もっと長く健康に動かし続ける」ためのものなんです。4列目の爬虫類、そしていつか5列目、6列目が加わっても、速水さんのシステムは柔軟に受け入れてくれるはずですよ。ぱうずハート動物病院の動物たちのために、これからも素敵なコードを書いてくださいね。

——ナナコ

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。