Featured image of post コード探偵ロックの事件簿【Strategy】巨大すぎる神の暴走〜終わらないif文からの脱出〜

コード探偵ロックの事件簿【Strategy】巨大すぎる神の暴走〜終わらないif文からの脱出〜

「すべてを1つのメソッドで処理する巨大なコード」に苦しむワトソン君。コード探偵ロックがStrategyパターンで神オブジェクトの『におい』を消し去る!

「レガシー・コード・インベスティゲーション(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』を外から受け取って、ただ『実行しろ』と命令するだけになった」

抽象的なStrategyパターンの概念図。中央の制御ユニットが交換可能なアルゴリズムモジュールを知的に入れ替えている、フラットでモダンなTechデザイン。

私は画面を見て息を呑んだ。 あの禍々しい 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 パターン。アルゴリズム(振る舞い)をそれぞれのクラスにカプセル化し、実行時に切り替える設計方式。責務の分離と拡張。新しいルールが増えても元のコードはいじらない(オープン・クローズドの原則)。テストも格段に書きやすくなる。

推理のステップ

  1. インターフェースの抽出: まず、すべての処理が守るべき共通のルール(Role)を定義する。
  2. 具象Strategy群の実装: 巨大な分岐の中にあった処理を、一つ一つの独立したクラスとして分離する。
  3. Contextからの委譲(Delegate): メインロジックからは分岐を消去し、外から渡されたStrategy(道具)に対して「実行せよ」とだけ命令する形にする。

ロックより

君のコードから漂っていたあのひどい『におい』。神を気取ったオブジェクトの傲慢さが、システム全体を窒息させていたことに気づけてよかったね、ワトソン君。 もしまた、同じような処理が大量の分岐でひしめき合っているコードを見つけたら、この「Strategy」という私の推理を思い出すといい。

さて、キーボードを叩く音はもう止んだ。次の事件が私を呼んでいる。

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