「レガシー・コード・インベスティゲーション(LCI)」。雑居ビルの2階にあるその怪しげな事務所のドアを叩いたとき、私はすでに限界だった。
前任者から引き継いだWebサービスの保守運用。最初は「ちょっとしたデータ出力機能の追加」のはずだった。だが、蓋を開けてみると、そこには「5000行を超える巨大なメソッド」が鎮座し、あらゆる分岐と処理をその身に宿して暴走していたのだ。
「……なるほど。典型的な『神オブジェクト(God Class)』のにおいだね」
事務所の主、自称コード探偵のロックは、モニター越しのコードを一瞥するなりそう言い放った。ヨレヨレのトレンチコートを羽織り、なぜか片手にはエナジードリンクを持っている。
「神オブジェクト……ですか?」
「ああ。彼は全知全能になろうとして、自重で押し潰されたのさ。さあワトソン君、現場(サーバー)を見せたまえ」
私は依頼人のはずだが、いつの間にか「ワトソン君」という称号を与えられ、彼の助手を務めることになってしまったようだ。
現場検証:コードの指紋
ロックはデスクトップに映し出されたコードを、不敵な笑みを浮かべながら見つめている。
「ワトソン君、この process_everything というメソッドを見てみたまえ。CSV、JSON、XML……ありとあらゆるフォーマットの処理が、巨大な if - elsif の壁の中に幽閉されている」
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
43
44
| #!/usr/bin/env perl
use v5.34;
use warnings;
use utf8;
use feature 'signatures';
no warnings "experimental::signatures";
package DataProcessor {
use Moo;
# すべての処理を一点で引き受ける「神のメソッド」
sub process_everything ($self, $type, $data) {
if ($type eq 'csv') {
# CSV専用のバリデーション
die "Missing 'name' for CSV" unless exists $data->{name};
die "Missing 'age' for CSV" unless exists $data->{age};
# CSVフォーマットに変換
my $csv_line = sprintf("%s,%s", $data->{name}, $data->{age});
return "CSV Output: $csv_line";
} elsif ($type eq 'json') {
# JSON専用のバリデーション
die "Missing 'id' for JSON" unless exists $data->{id};
die "Missing 'value' for JSON" unless exists $data->{value};
# 手動で簡易JSON風フォーマット生成
my $json_string = sprintf('{"id":%d,"value":"%s"}', $data->{id}, $data->{value});
return "JSON Output: $json_string";
} elsif ($type eq 'xml') {
# XML専用のバリデーション
die "Missing 'root' for XML" unless exists $data->{root};
die "Missing 'content' for XML" unless exists $data->{content};
# 簡易XML風フォーマット生成
my $xml_string = sprintf('<%s>%s</%s>', $data->{root}, $data->{content}, $data->{root});
return "XML Output: $xml_string";
} else {
die "Unknown type: $type";
}
}
}
|
「確かに酷いコードです。でも、新しいフォーマット……例えばYAMLを追加しろって言われたら、どうすればいいんでしょうか? この巨大な if 文の塊に、さらに elsif ($type eq 'yaml') を書き足すしかないんですか……?」
「その通りさ。そして君は、いつかYAMLのパース処理をミスして、CSVの出力まで道連れにクラッシュさせることになる」
「えっ……でも、それぞれ別のブロックに分かれているのに、どうして……?」
「同じ手枷足枷に繋がれているからさ。すべてのコードが密結合し、一触即発の状態にある。これが『におい』の正体だよ」
ロックはエナジードリンクを一口飲むと、キーボードに手を伸ばした。
「神を人間サイズに分割する時間だ」
推理披露:鮮やかなリファクタリング
「まず犯人は、自分が何を処理しているのかを知りすぎている。アルゴリズム——つまり、CSVやJSONへの変換という『具体的な振る舞い』をオブジェクトとして切り出すんだ」
ロックの指が叩き出すコードは、まるで魔法のように複雑な if 文を切り刻んでいく。
1. 共通インターフェースの約束
「まず、すべての処理方針(Strategy)が守るべき『約束』を定義するんだ。どんなフォーマットだろうと、『処理する(process)』ことだけを知っていればいい」
1
2
3
4
5
6
7
| # ------------------------------
# 1. 共通インターフェース (Role)
# ------------------------------
package Processor::Role {
use Moo::Role;
requires 'process';
}
|
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
| # ------------------------------
# 2. 具体的なStrategy群
# ------------------------------
package Processor::Csv {
use Moo;
with 'Processor::Role';
sub process ($self, $data) {
die "Missing 'name' for CSV" unless exists $data->{name};
die "Missing 'age' for CSV" unless exists $data->{age};
my $csv_line = sprintf("%s,%s", $data->{name}, $data->{age});
return "CSV Output: $csv_line";
}
}
package Processor::Json {
use Moo;
with 'Processor::Role';
sub process ($self, $data) {
die "Missing 'id' for JSON" unless exists $data->{id};
die "Missing 'value' for JSON" unless exists $data->{value};
my $json_string = sprintf('{"id":%d,"value":"%s"}', $data->{id}, $data->{value});
return "JSON Output: $json_string";
}
}
|
3. Context(メインロジック)の解放
「さて、ワトソン君。主役の登場だ。神であったはずの DataProcessor は、今やすべての責務を部下に委譲(Delegate)するスマートな上司に生まれ変わった」
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # ------------------------------
# 3. Context (利用側・メインロジック)
# ------------------------------
package DataProcessor {
use Moo;
# 実行時に適切なStrategyを注入し、すべての処理を委譲(Delegate)する
# これにより、DataProcessor自身からは巨大な if 文が消滅する。
sub execute ($self, $strategy, $data) {
# Strategyが正しいRoleを持っているか軽くチェックする
die "Invalid strategy" unless $strategy->DOES('Processor::Role');
return $strategy->process($data);
}
}
|
「……ええと? クラスが分かれたのは分かりますが、これだと DataProcessor はどうやって処理を切り替えるんですか?」
「そこさ。このスマートな上司は、部下である『Strategy』を外から受け取って、ただ『実行しろ』と命令するだけになった」

私は画面を見て息を呑んだ。
あの禍々しい if - elsif の連鎖が、跡形もなく消え去っている。
「……待って。もしかして、新しい『YAMLの処理』を追加したいときは……」
「そう、既存の DataProcessor を1行も書き換える必要はない。新しいクラス(Processor::Yaml)を作って、この上司に派遣してやるだけでいい」
「そうか……! 処理の分岐そのものを、オブジェクトの切り替えにすり替えたんですね!」
「その通りだ。アルゴリズムを家族から切り離し、自由に交換可能にする。これが Strategy(戦略)パターン という名の切り札さ」
1
2
3
4
5
6
7
8
9
10
| # 動作確認時の呼び出し方
my $processor = DataProcessor->new;
# CSVとして処理したい場合
my $csv_strategy = Processor::Csv->new;
$processor->execute($csv_strategy, { name => 'Watson', age => 28 });
# JSONとして処理したい場合
my $json_strategy = Processor::Json->new;
$processor->execute($json_strategy, { id => 101, value => 'Code Detective' });
|
事件の終わり:平和なビルド
ロックによって解き放たれたコードは、それぞれの単体テストが見事に通り、平穏を取り戻した。
「すごい……まるで魔法みたいです。これであの5000行ともお別れなんですね!」
「初歩的なことだよ、ワトソン君。すべての不吉な if 構文を排除して残ったものが、この『Strategy』という真実なんだよ」
ロックは満足げにコートの襟を立てた。
「さて、事件は解決した。報酬のあの『長いメソッドの行数と同じミリ数のバーボン』……の代わりのエナジードリンクをいただこうか。次はどんな不吉なにおいがする現場へ私を案内してくれるのかな?」
私は苦笑しながら、彼に新品の缶を差し出した。
どうやら、私の「ワトソン君」としての生活は、まだ始まったばかりらしい。
探偵の調査報告書
| 容疑(アンチパターン) | 真実(パターン) | 証拠(効果) |
|---|
| 神オブジェクト(God Class)。何でも知っていて何でもやるクラス。巨大な if - else の要塞を生み出す。 | Strategy パターン。アルゴリズム(振る舞い)をそれぞれのクラスにカプセル化し、実行時に切り替える設計方式。 | 責務の分離と拡張。新しいルールが増えても元のコードはいじらない(オープン・クローズドの原則)。テストも格段に書きやすくなる。 |
推理のステップ
- インターフェースの抽出: まず、すべての処理が守るべき共通のルール(Role)を定義する。
- 具象Strategy群の実装: 巨大な分岐の中にあった処理を、一つ一つの独立したクラスとして分離する。
- Contextからの委譲(Delegate): メインロジックからは分岐を消去し、外から渡されたStrategy(道具)に対して「実行せよ」とだけ命令する形にする。
ロックより
君のコードから漂っていたあのひどい『におい』。神を気取ったオブジェクトの傲慢さが、システム全体を窒息させていたことに気づけてよかったね、ワトソン君。
もしまた、同じような処理が大量の分岐でひしめき合っているコードを見つけたら、この「Strategy」という私の推理を思い出すといい。
さて、キーボードを叩く音はもう止んだ。次の事件が私を呼んでいる。