Featured image of post コード探偵ロックの事件簿【Template Method】瓜二つの容疑者〜コピペ双子の分離手術〜

コード探偵ロックの事件簿【Template Method】瓜二つの容疑者〜コピペ双子の分離手術〜

双子エンジニアが持ち込んだ「瓜二つのコード」。コード探偵ロックがTemplate Methodパターンでコピペの呪いを解く!

その日、「レガシー・コード・インベスティゲーション(LCI)」のドアは、同時に二度ノックされた。

「すみません、コードの相談を——」 「すみません、コードの相談を——」

私たちは顔を見合わせた。双子の姉妹が、同じタイミングで同じ事務所のドアを叩き、同じセリフを口にする。……まあ、よくあることだ。

私はアオイ。隣にいるのは妹のミドリ。同じ会社のバックエンドチームで、それぞれ別のデータのCSVエクスポート機能を担当している。

事務所の奥から、例のヨレヨレのトレンチコートが現れた。

「ほう。双子か」

コード探偵——自称——ロック。エナジードリンクを片手に、私たちを値踏みするように見つめている。

以降、私は心の中でこの男を「トレンチコート」と呼ぶことにした。だってそれ以外に印象がない。

「私はアオイです。こちらは妹の——」 「ミドリです」 「問題ない。2人ともワトソン君だ」

……いいえ、それは問題です。

「で、ワトソン君たち。何があった?」 「CSVエクスポートのバグを直したんですが——」 「直したら、もう片方にも同じ修正が必要で——」

トレンチコートが片眉を上げた。

「2人が同時に同じセリフを言うだけでなく、同じ悩みまで持ち込むとは。なかなか面白い事件だ」


現場検証:瓜二つの指紋

トレンチコートは2つのモニターに、それぞれのコードを映し出した。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package UserCsvExporter {
    use Moo;

    sub export ($self) {
        # 1. データ取得
        my @users = (
            { name => 'Aoi', email => 'aoi@example.com' },
            { name => 'Midori', email => 'midori@example.com' },
        );

        # 2. ヘッダー行の生成
        my @lines = ('name,email');

        # 3. データ行の整形
        for my $row (@users) {
            push @lines, sprintf('%s,%s', $row->{name}, $row->{email});
        }

        # 4. 出力文字列の結合
        return join("\n", @lines) . "\n";
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package OrderCsvExporter {
    use Moo;

    sub export ($self) {
        # 1. データ取得
        my @orders = (
            { order_id => 1001, product => 'Keyboard', price => 8500 },
            { order_id => 1002, product => 'Trackball', price => 6200 },
        );

        # 2. ヘッダー行の生成
        my @lines = ('order_id,product,price');

        # 3. データ行の整形
        for my $row (@orders) {
            push @lines, sprintf('%d,%s,%d',
                $row->{order_id}, $row->{product}, $row->{price});
        }

        # 4. 出力文字列の結合
        return join("\n", @lines) . "\n";
    }
}

2つのコードが横に並んだ瞬間、トレンチコートの口元が緩んだ。

「ほう……依頼人だけでなく、コードまで双子とは。これは興味深い」

「え? 全然違うコードですよ!」ミドリが身を乗り出す。 「そうです、扱うデータが違いますし……」私も続けた。

トレンチコートは人差し指を立てて、2つのコードを交互に指した。

「いいや。骨格は同じだ。彼らは変装しているだけさ。ほら、ここを見たまえ——データ取得、ヘッダー生成、行の整形、出力の結合。手順は完全に瓜二つだ」

言われてみれば、確かに——1、2、3、4のステップは、テーブル名やカラム名が違うだけで、流れは同じだ。

「……でも、それぞれ動いてはいるんです」 「ああ、今はね。だが君たちはさっき言ったはずだ——バグを直すたびに、もう片方にも同じ修正が必要だと」

トレンチコートはエナジードリンクを一口飲んだ。

「これは『コピペコード(Duplicated Code)』の典型的なにおいだよ、ワトソン君たち。そしてこのにおいは、放っておくと増殖する」


推理披露:家族の再統合

「解決策はシンプルだ。双子の『同じ部分』を親に引き上げ、『違う部分』だけを子に残す」

トレンチコートのキーボードが鳴り始めた。

1. 骨格の抽出

「まず、この2つのコードに共通する『手順書』を基底クラスに定義する。これが Template Method ——つまり『処理の骨格=テンプレート』を親が持つということだ」

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package CsvExporter::Base {
    use Moo;

    # Template Method: 処理の骨格を定義
    sub export ($self) {
        my @data  = $self->fetch_data();
        my @lines = ($self->header_line());

        for my $row (@data) {
            push @lines, $self->format_row($row);
        }

        return join("\n", @lines) . "\n";
    }

    # サブクラスでオーバーライドすること
    sub fetch_data ($self)       { die ref($self) . "::fetch_data must be overridden" }
    sub header_line ($self)      { die ref($self) . "::header_line must be overridden" }
    sub format_row ($self, $row) { die ref($self) . "::format_row must be overridden" }
}

「あ、全部のメソッドをサブクラスに移せばいいんですね!」ミドリが声を上げた。

トレンチコートは首を振った。

「いいや。共通部分は親に残すんだ。export メソッドの流れ——データを取り、ヘッダーを作り、行を整形して結合する——この骨格は1箇所に集約する。差分だけを子に委ねる。それがTemplate Methodだよ、ワトソン君」

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
29
30
31
32
33
34
35
package CsvExporter::User {
    use Moo;
    extends 'CsvExporter::Base';

    sub fetch_data ($self) {
        return (
            { name => 'Aoi',   email => 'aoi@example.com' },
            { name => 'Midori', email => 'midori@example.com' },
        );
    }

    sub header_line ($self) { 'name,email' }

    sub format_row ($self, $row) {
        return sprintf('%s,%s', $row->{name}, $row->{email});
    }
}

package CsvExporter::Order {
    use Moo;
    extends 'CsvExporter::Base';

    sub fetch_data ($self) {
        return (
            { order_id => 1001, product => 'Keyboard',  price => 8500 },
            { order_id => 1002, product => 'Trackball', price => 6200 },
        );
    }

    sub header_line ($self) { 'order_id,product,price' }

    sub format_row ($self, $row) {
        return sprintf('%d,%s,%d', $row->{order_id}, $row->{product}, $row->{price});
    }
}

「……待って」

私は画面を凝視していた。何かが引っかかる。

「この export メソッドの中で fetch_dataheader_line を呼んでいますけど……これって、親クラスが子クラスのメソッドを呼んでいるってこと……ですか?」

「いい質問だ、ワトソン君A」

Template Methodパターンの概念図。親の設計図の手順を受け継ぎ、子がそれぞれの個性で差分を埋めている、フラットでモダンなTechデザイン。

——Aは余計です。

「その通り。親クラスは手順だけを知っている。具体的にどんなデータを取るか、どんなヘッダーにするかは、子に任せる。つまり——」

骨格は一つ、中身は自由に差し替えられる……!」

「ご名答。そしてもう一つ。バグが見つかったとき、骨格の修正は——?」

ミドリと私は同時に答えた。

「親クラスの1箇所だけ!」

……また声が揃ってしまった。トレンチコートがニヤリと笑う。

「双子というのは、コードにも伝染するらしいね」


事件の終わり:2つのテストが通る日

テストがすべてグリーンに変わった。Before版とAfter版の出力は完全に一致し、振る舞いは何も壊れていない。

「これで……片方を直してもう片方を忘れる、なんてことはなくなるんですね」

「私たちは双子だけど、コードまで双子にする必要はなかったのね」ミドリが溜息をついた。

「初歩的なことだよ、ワトソン君たち」

トレンチコートはコートの襟を立てた(室内は相変わらずPCの排熱で暑いのに)。

「さて、報酬だが——同じブレンドだが焙煎度だけ違うコーヒー豆を2袋いただこうか。骨格は同じ、差分は細部。まさにTemplate Methodだろう?」

私とミドリは顔を見合わせた。

「……ロックさん、それ普通にコーヒー豆が2袋欲しいだけですよね」 「失敬な。これはパターンの本質を体現したメタファーだよ」

私たちは呆れながらも、後日きっちり2袋を届けることになるのだった。 双子の依頼人と双子のコード。解決してみれば笑い話だが、あの瞬間のトレンチコートの推理は——悔しいけれど、鮮やかだった。


探偵の調査報告書

容疑(アンチパターン)真実(パターン)証拠(効果)
コピペコード(Duplicated Code)。ほぼ同じ処理が複数箇所にコピペされ、バグ修正も二重に必要になる。Template Method パターン。処理の骨格(テンプレート)を親クラスに定め、差分だけをサブクラスでオーバーライドする。DRY原則の達成。共通ロジックの修正は1箇所で済む。新しいエクスポート形式の追加もサブクラス1つで完了。

推理のステップ

  1. 共通骨格の発見: 複数のコードに共通する「手順」(データ取得→ヘッダー→整形→出力)を特定する。
  2. 基底クラスへの抽出: 共通の手順を export メソッドとして基底クラスに集約する。この export が Template Method。
  3. 差分のオーバーライド: 各サブクラスは fetch_dataheader_lineformat_row だけを定義すればよい。

ロックより

瓜二つに見えるものほど、その違いにこそ価値がある。 君たちの双子のコードは、実は「同じであるべき部分」と「異なるべき部分」の区別がついていなかったんだ。Template Methodは、その境界線を引くための推理の道具さ。

今度コピペしたくなったら、まず自分に問いかけたまえ——「この骨格は、本当に2つ必要か?」

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