Featured image of post コードドクター【Prototype】遺伝子異常の連鎖〜テンプレ完璧症候群と原型の処方箋〜

コードドクター【Prototype】遺伝子異常の連鎖〜テンプレ完璧症候群と原型の処方箋〜

「完璧な設計図」を信じて17種類のnewを書き続けたSIerの女性エンジニア。フィールド追加のたびに全身に転移する遺伝子異常。コードドクターが暴いた「毎回newする病」の処方箋——Prototypeパターン。

「私の設計は正しい。だって設計書通りに作ったんですから」

10年間、この言葉を盾にしてきた。

入社3年目に作った「ドキュメント生成スクリプト」は、いつの間にか社内標準ツールに昇格していた。議事録、日報、週報、障害報告書——17種類のテンプレートを自動生成するこのツールは、私の最高傑作だ。SIer歴10年、サブリーダー。34歳。後輩たちには「三橋さんのツール、毎日使ってます」と感謝され、私のデスクの付箋には改善要望が日々貼られている。

昨日、上長からSlackが飛んできた。

「来期から全ドキュメントに approval_status フィールド追加ね。コンプライアンス対応」

1フィールドの追加。簡単な作業のはずだった。

コードを開いた。17種類のテンプレートクラス。それぞれのコンストラクタに、全フィールドがハードコードされている。approval_status を追加するには、17箇所の new_template メソッドを、1箇所ずつ、手で、直す必要がある。

4時間かけて全部直した——はずだった。

翌朝、経理部から連絡が来た。「障害報告書のフォーマットが壊れてます」。確認すると、3箇所の修正漏れ。メタデータの division フィールドが空になっている障害報告書が、お客様に送られていた。

「完璧な設計書通り」に作ったはずの私のコードが、1フィールドの追加で崩壊した。

来院

「コード診療所」——同僚の木村が教えてくれた場所だ。雑居ビルの2階にあるらしい。

「三橋さんのツール、一度診てもらったほうがいいですよ。俺も前に行ったことあるんですけど、マジで腕がいいんで」

正直、私のコードに問題があるとは思っていない。設計書通りに作った。テストも通っている。おそらく環境の問題——Perlのバージョンか、モジュールの依存関係か。プロの目で確認してもらえれば、そう証明できるはずだ。

磨かれたリノリウムの廊下を進む。重厚な鉄の扉に、「コード診療所」とだけ書かれたプレート。

ドアを引いた瞬間、O’Reillyの技術書が視界を埋め尽くした。天井近くまで積み上げられた本の壁。その隙間から、受付カウンターが覗いている。

「いらっしゃいませ。ご予約の方ですね?」

白衣を着た女性——助手のナナコさんが、穏やかに立ち上がった。

「はい。三橋です。ドキュメント生成ツールの件で」

「大丈夫ですよ、ここはコード診療所です。症状をお聞かせくださいね」

ナナコさんの背後。トリプルディスプレイに向かう男の背中。HHKBの打鍵音がぴたりと止まった。

「……何種類?」

振り返りもしない。低い声だけが聞こえた。

「え?」

ナナコさんが微笑んだ。「テンプレートの種類数をお聞きしていますよ」

「17……種類、です」

ドクターの肩が微かに揺れた。溜息——のようにも聞こえたが、背中からでは判別がつかない。

触診

ドクターがゆっくりと椅子を回転させた。初めて顔が見えた。鋭い目つき。表情は無い。

「見せろ」

私はノートPCを開き、コードを表示した。

 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 DocumentTemplate::Minutes;
use v5.36;

sub new_template($class, %args) {
    return {
        type       => '議事録',
        department => $args{department} // '開発部',
        author     => $args{author}    // '未設定',
        date       => $args{date}      // '2025-01-01',
        version    => '1.0',
        metadata   => {
            company     => '株式会社テックソリューション',
            division    => $args{department} // '開発部',
            fiscal_year => '2025',
            confidential => 1,
        },
        sections => [
            { title => '出席者',   content => '' },
            { title => '議題',     content => '' },
            { title => '決定事項', content => '' },
            { title => '次回予定', content => '' },
        ],
    };
}

ドクターがスクロールした。次のクラス。また同じ構造の new_template。その次も。その次も。17クラス。company も division も fiscal_year も、全て同じ値が17回繰り返されている。

指がピタリと止まった。

「……17回。毎回 new

「はい。各テンプレートは独立しているべきですから。設計書にもそう書いて——」

「遺伝子異常(Genetic Disorder)」

診断

ナナコさんが穏やかに、しかし的確に補足した。

「先生がおっしゃっているのは、遺伝子異常の連鎖ですね。同じ 遺伝子 ——共通フィールドが、17の細胞——テンプレートクラスにハードコードされています。1つの遺伝子変異—— approval_status の追加が入ると、17の細胞すべてに転移するんです」

「でも、それぞれ独立したクラスにするのが正しい設計——」

ドクターが無言で画面をスクロールした。metadata のブロック。17クラス、完全に同一のハッシュリファレンス。

「……転移」

ナナコさんが頷いた。「17の臓器に、まったく同じ遺伝子がハードコードされているんです。companyfiscal_year も、全部同じですよね? 1箇所を変異——修正すれば済むはずのものが、17箇所に分散しているから、修正漏れが起きるんですよ」

「3箇所の修正漏れで障害報告書が壊れたのは——」

「はい。転移した遺伝子異常が、3つの臓器で発症したということですね」

ドクターがさらにスクロールした。ある箇所で指が止まる。

「……癒着」

ナナコさんの表情が少し厳しくなった。

「もうひとつ、副症状がありますね。先生が指しているのは metadata のハッシュリファレンスです。もし誰かがこのテンプレートを “コピー” して使おうとした場合——浅いコピーでは、ネストされた metadata が参照を共有します。一方を変更すると、もう一方も壊れる。臓器の癒着ですね」

「え……そこ、バグ出てませんけど……」

「……まだ」

ドクターの一言が、診察室に重く響いた。

ナナコさんが小さく付け加えた。「時限爆弾ですよ。今は発症していないだけです」

私は黙った。「完璧な設計」の中に、2つの病巣が潜んでいた。

処方箋

ドクターが振り返り、トリプルディスプレイに向き直った。黙々とキーボードを叩き始める。

ナナコさんが私の隣に来て、画面を指差しながら解説してくれた。

「先生は今から、 原型——プロトタイプ を作ります。17の設計図を毎回引き直すのではなく、完成品を1つ保管して、必要な時に複製するんですよ」

原型の培養

まず、すべてのテンプレートの「原型」となるクラスが定義された。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package DocumentTemplate::Prototype;
use v5.36;
use Moo;
use Storable qw(dclone);

has type       => (is => 'ro', required => 1);
has department => (is => 'ro', default  => '開発部');
has author     => (is => 'ro', default  => '未設定');
has date       => (is => 'ro', default  => '2025-01-01');
has version    => (is => 'ro', default  => '1.0');
has metadata   => (is => 'ro', default  => sub {
    {
        company      => '株式会社テックソリューション',
        division     => '開発部',
        fiscal_year  => '2025',
        confidential => 1,
    }
});
has sections   => (is => 'ro', default => sub { [] });

「ここが遺伝子の 原本 です」ナナコさんが画面を指差した。「共通フィールドは 1箇所だけ に定義されます。17箇所のコピペは、もう必要ありません」

「でも、テンプレートごとに sections は違いますよね? 議事録は『出席者・議題・決定事項・次回予定』で、日報は『本日の作業・進捗・課題・明日の予定』で——」

「ええ。だから clone するんです」

複製の処方

ドクターの指が clone メソッドを打ち込んでいく。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
sub clone($self, %overrides) {
    my $data = dclone({
        type       => $self->type,
        department => $self->department,
        author     => $self->author,
        date       => $self->date,
        version    => $self->version,
        metadata   => $self->metadata,
        sections   => $self->sections,
    });

    for my $key (keys %overrides) {
        if (ref $data->{$key} eq 'HASH'
            && ref $overrides{$key} eq 'HASH') {
            $data->{$key} = {
                $data->{$key}->%*, $overrides{$key}->%*
            };
        } else {
            $data->{$key} = $overrides{$key};
        }
    }

    return (ref $self)->new($data->%*);
}

dclone」ナナコさんが言った。「Storable モジュールの 深層複製 ですね。ネストされた metadatasections も、参照共有ではなく完全に独立したコピーを作ります。先ほどの癒着——shallow copy の時限爆弾は、これで除去されますよ」

dclone で全部コピーしてから、%overrides で差分だけ上書き……」

「そうです。原型を壊さず、必要な部分だけカスタマイズするんです」

保管庫の構築

続いて、プロトタイプの「保管庫」—— Registry が組み上がっていく。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package DocumentTemplate::Registry;
use v5.36;
use Moo;

has _prototypes => (is => 'ro', default => sub { {} });

sub register($self, $name, $prototype) {
    $self->_prototypes->{$name} = $prototype;
    return $self;
}

sub create($self, $name, %overrides) {
    my $proto = $self->_prototypes->{$name}
        // die "未登録のテンプレート: $name";
    return $proto->clone(%overrides);
}

register で原型を保管し、create で複製する。シンプルですね」ナナコさんが微笑んだ。

外科手術

ドクターがデフォルトのレジストリを構築していく。

 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
sub build_default($class) {
    my $registry = $class->new;

    $registry->register('議事録' =>
        DocumentTemplate::Prototype->new(
            type     => '議事録',
            sections => [
                { title => '出席者',   content => '' },
                { title => '議題',     content => '' },
                { title => '決定事項', content => '' },
                { title => '次回予定', content => '' },
            ],
    ));

    $registry->register('日報' =>
        DocumentTemplate::Prototype->new(
            type     => '日報',
            sections => [
                { title => '本日の作業', content => '' },
                { title => '進捗状況',   content => '' },
                { title => '課題・問題', content => '' },
                { title => '明日の予定', content => '' },
            ],
    ));

    # ... 障害報告書、週報、他のテンプレートも同様に登録

    return $registry;
}

「あ」

私は声を上げた。17クラスに散らばっていた共通フィールド—— departmentauthordateversionmetadata——が、すべて Prototype クラスの1箇所に集約されている。各テンプレートの register では、 違う部分だけ ——typesections だけを指定している。

「テンプレートを使うときはこうなります」ナナコさんが画面を指差した。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
my $registry = DocumentTemplate::Registry->build_default;

# 営業部の議事録を生成
my $doc = $registry->create('議事録',
    department => '営業部',
    author     => '田中',
    date       => '2025-06-15',
    metadata   => { division => '営業部' },
);

# 別の議事録——完全に独立したインスタンス
my $doc2 = $registry->create('議事録',
    department => '経理部',
    author     => '山田',
    metadata   => { division => '経理部' },
);

docdoc2 は完全に独立しています。deep clone ですから、metadata を変更しても——」

「もう一方には影響しない……」

「そうです。癒着は解消されました」

ドクターがテストを実行した。画面に結果が流れる。

1
2
3
4
5
ok 1 - doc1の部署は元のまま
ok 2 - doc2の部署は変更済み
ok 3 - doc1のメタデータは独立
ok 4 - doc2のメタデータも独立
ok 5 - doc1のセクションは影響を受けない

全テスト、グリーン。

「そして」ナナコさんが付け加えた。「来期の approval_status 追加ですが——」

Prototype クラスに has approval_status を1行追加するだけ……?」

「はい。17箇所の手修正は、もう必要ありませんよ」

術後経過

修復されたコードは、驚くほどシンプルだった。

17個の独立したクラスファイルは、1つの Prototype クラスと1つの Registry に統合された。フィールド追加は原型を1箇所変えるだけ。各テンプレートは clone で原型の遺伝子を受け継ぎ、sections だけがそれぞれの個性を持つ。

私はノートPCを閉じ、帰り支度を始めた。PCを鞄にしまおうとした拍子に、天板に貼っていた付箋——後輩たちからの改善要望メモ——が1枚、ひらりと滑り落ちた。

拾おうとしたが、ドクターのほうが速かった。一瞬——ほんの一瞬だけ、メモに目を落とした。そして何も言わずに、天板に貼り直した。

(……先生が、私のメモを読んだ?)

胸が少し熱くなった。後輩たちの要望が書かれたあのメモ。「日報に『気づき欄』を追加してほしい」「障害報告書にSeverityフィールドがほしい」。私が10年間、一枚一枚集めてきた声。

(まさか先生は、あのメモの内容を覚えた—— clone した——のでは。私のチームの声を、先生の頭の中にも複製してくれた……?)

ナナコさんが苦笑した。

「先生はメモを拾っただけですよ。風で飛ばないように戻しただけだと思います」

ドクターは既にトリプルディスプレイに向き直っていた。視線が一瞬泳いだように見えたのは、おそらく気のせいだ。

ドクターが背を向けたまま、ぽつりと言った。

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

ナナコさんが穏やかに微笑んだ。

「先生が求めるのはお金ではなく、コード品質へのコミットメントです。17個の new を、もう増やさないでくださいね。お大事に」

重厚な鉄の扉を押して廊下に出ると、HHKBの打鍵音が遮断された。

私は廊下で立ち止まり、スマホを開いた。Slackの上長のメッセージが表示されている。

「来期から全ドキュメントに approval_status フィールド追加ね」

10年前の私なら、17個のクラスファイルを開いて、1つずつ approval_status を書き加えていただろう。

今日の私は、Prototype クラスに has approval_status を1行追加する。

「完璧な設計図」を信じていた。でも本当に「完璧」だったのは、完成品を1つ作り、それを複製して差分だけを変える——という、もっとシンプルな原則のほうだった。

17種類の子供たちは、もう独立して生きていく必要がない。原型の遺伝子を受け継ぎながら、それぞれの個性だけを纏って。


処方箋まとめ

症状適用すべき経過観察
同じフィールド構成のオブジェクトを複数箇所で new している
フィールド追加のたびに複数クラスを手修正する必要がある
ハッシュリファレンスの浅いコピーでネストデータが参照共有している
テンプレートの「種別」は違うが「構造」はほぼ同じ
オブジェクトの構造がシンプルで種別が2〜3種類しかない
各オブジェクトの構造が根本的に異なり、共通部分がほぼない

治療のステップ

  1. 原型(Prototype)クラスの定義 — 共通フィールドを1つのクラスに集約し、clone メソッドを実装
  2. 深層複製の導入Storable::dclone を使い、ネスト構造も含めた完全独立なコピーを保証
  3. 差分カスタマイズclone(%overrides) で変更が必要な部分だけを上書き
  4. Registry の構築 — テンプレートの原型を名前で管理し、create で複製を取得
  5. 既存クラスの統合 — 17個の独立した new_template を、Registry からの create に順次置換

助手より

「設計書通りに作ったから正しい」——その信念は、決して間違いではありませんでした。ただ、設計にはもう1つ大切な原則があります。 「同じことを繰り返さない」 ということです。

先生は無口ですが、患者さんの付箋メモを拾ったとき、あの一瞬だけ目を落としていたのは事実ですよ。もしかしたら後輩の方々の声が、先生の中にも少しだけ残ったのかもしれませんね。

10年間育ててきたツールは、これからもっと育てやすくなるはずです。17の子供たち、良い遺伝子を受け継いでくれますように。お大事に。

——ナナコ

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