Featured image of post コード探偵ロックの事件簿【Prototype】複製される完璧な設計図〜17枚の帳票はなぜ同時に壊れたか〜

コード探偵ロックの事件簿【Prototype】複製される完璧な設計図〜17枚の帳票はなぜ同時に壊れたか〜

完璧だと信じて17種類の帳票テンプレートを毎回 new していたら、項目追加ひとつで全身崩壊。散在した設計図を「Prototypeパターン」で原型に戻す、コード探偵ロックの推理。

「助けてください! 帳票テンプレートに項目を1つ足しただけなのに、17種類の出力が次々に壊れたんです!」

私はSIerで社内ツールを担当している三橋彩香。障害報告書、議事録、日報、週報。部署ごとに微妙に違う帳票を自動生成するPerlスクリプトは、入社3年目の私が作り、10年かけて磨き上げてきた自信作だった。

その誇りが、昨日の夕方までは。

「全帳票に approval_status を追加してください。コンプライアンス部からの指示です」

たったそれだけの依頼だった。私は余裕の表情でエディタを開き、17種類のテンプレート定義を順番に修正した。全部直した。そう思っていた。

だが今朝、総務部から電話が鳴った。

「障害報告書だけ division が空欄です。しかも議事録の機密フラグまでおかしくなっています」

私はノートPCを抱え、雑居ビルの薄暗い階段を駆け上がっていた。ガラス扉には、今日も怪しげな文字が浮かんでいる。

「レガシー・コード・インベスティゲーション(LCI)」

ドアを開けると、サーバーラックの排熱とエナジードリンクの甘ったるい残り香が鼻を刺した。革張りの椅子にふんぞり返る男――自称「コード探偵」のロックは、古びたメカニカルキーボードを打つ手を止め、私の顔より先にノートPCの角を見た。

「おやおや、ワトソン君。ずいぶん重そうな顔をしている。コードか、責任か、どちらを抱えてきたのかね」

「三橋です。責任はともかく、コードは確実に重いです」

私は椅子に腰を下ろすなりPCを開いた。

「テンプレートは全部、設計書どおりに作ってあるんです。でも項目追加ひとつで、17種類の帳票が連鎖的に壊れて……」

ロックは画面を一瞥し、眉ひとつ動かさず言った。

「初歩的なにおいだよ。君は完成品の複製で済む仕事を、毎回ゼロから組み立て直している」

現場検証:増殖する完璧な設計図

「見てください。帳票テンプレートは種類ごとにメソッドを分けています。独立していたほうが安全だと思って……」

【Before】テンプレート定義が散在したコード

 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
45
46
47
package DocumentTemplates;
use Moo;

sub create_minutes_template ($self, %args) {
    return {
        type       => '議事録',
        department => $args{department} // '開発部',
        author     => $args{author} // '未設定',
        date       => $args{date} // '2026-03-22',
        metadata   => {
            company         => '株式会社テックソリューション',
            division        => $args{department} // '開発部',
            fiscal_year     => '2026',
            approval_status => $args{approval_status} // 'draft',
            confidential    => 1,
        },
        sections => [
            { title => '出席者',   content => '' },
            { title => '議題',     content => '' },
            { title => '決定事項', content => '' },
        ],
    };
}

sub create_incident_report_template ($self, %args) {
    return {
        type       => '障害報告書',
        department => $args{department} // '開発部',
        author     => $args{author} // '未設定',
        date       => $args{date} // '2026-03-22',
        metadata   => {
            company         => '株式会社テックソリューション',
            division        => $args{department} // '開発部',
            fiscal_year     => '2026',
            approval_status => $args{approval_status} // 'draft',
            confidential    => 1,
        },
        sections => [
            { title => '発生時刻', content => '' },
            { title => '影響範囲', content => '' },
            { title => '暫定対応', content => '' },
            { title => '恒久対応', content => '' },
        ],
    };
}

1;

「実際にはこれが17種類あります。共通項目もありますけど、帳票ごとに独立していたほうがわかりやすいので……」

「わかりやすい?」

ロックは低く繰り返した。

「君は同じ設計図を17枚に手書きで複写している。1箇所の修正が17箇所に転移するのは当然だ」

「でも、複製しようとして浅いコピーにしたら、今度は別の帳票まで巻き込まれて……」

私は震える指で、応急処置として書いたコードを見せた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
my $minutes = $templates->create_minutes_template(
    author => '三橋彩香',
);

# これで複製したつもりだった
my $incident = { %{$minutes} };
$incident->{type} = '障害報告書';
$incident->{metadata}{division} = '運用部';

print $minutes->{metadata}{division};
# => 運用部
# 元の議事録テンプレートまで汚染される!

「うっ……ネストした metadata が参照共有になってました」

ロックはエナジードリンクの缶を机に置いた。

「つまり病巣は二つだ。ひとつは散在した原本。もうひとつは浅い複製。君はコピー機も設計図保管庫も持たず、手書きとトレーシングペーパーで現場を回していたのだよ」

推理披露:原型保管庫(Prototype Registry)の設置

ロックは私の手からPCを奪い取り、ターミナルを開いた。

「必要なのは、17人の転記係ではない。原型(Prototype) を保管し、必要なときに安全に複製する仕組みだ」

「原型……?」

「帳票を毎回ゼロから組み立てるのではない。完成済みのひな形を保管しておき、そこから複製して差分だけ変える。書類仕事の基本だろう?」

【After】Prototype を管理する Registry

 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
package DocumentPrototypeRegistry;
use Moo;
use Carp qw(croak);
use Storable qw(dclone);

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

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

sub create ($self, $name, %overrides) {
    my $prototype = $self->_prototypes->{$name}
        or croak "unknown prototype: $name";

    my $document = dclone($prototype);
    _merge_hash($document, \%overrides);
    return $document;
}

sub _merge_hash ($target, $overrides) {
    for my $key (keys %{$overrides}) {
        if (ref($target->{$key}) eq 'HASH'
            && ref($overrides->{$key}) eq 'HASH') {
            _merge_hash($target->{$key}, $overrides->{$key});
            next;
        }

        $target->{$key} = $overrides->{$key};
    }
}

1;

ロックはキーを叩きながら続けた。

「注目点は二つ。まず、register()原型そのものを保管していること。次に、create()dclone() を使って深い複製を作っていることだ。これで metadatasections のようなネストした構造も安全に独立する」

「じゃあ、帳票の共通部分は……?」

「最初から原型にまとめておくのだよ」

【After】原型を一度だけ登録する

 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
sub base_template {
    return {
        department => '開発部',
        author     => '未設定',
        date       => '2026-03-22',
        metadata   => {
            company         => '株式会社テックソリューション',
            division        => '開発部',
            fiscal_year     => '2026',
            approval_status => 'draft',
            confidential    => 1,
        },
    };
}

my $registry = DocumentPrototypeRegistry->new;

$registry->register(
    minutes => {
        %{ base_template() },
        type     => '議事録',
        sections => [
            { title => '出席者',   content => '' },
            { title => '議題',     content => '' },
            { title => '決定事項', content => '' },
        ],
    },
);

$registry->register(
    incident_report => {
        %{ base_template() },
        type     => '障害報告書',
        sections => [
            { title => '発生時刻', content => '' },
            { title => '影響範囲', content => '' },
            { title => '暫定対応', content => '' },
            { title => '恒久対応', content => '' },
        ],
    },
);

approval_status を増やしたければ、base_template() を1箇所直せばいい。17種類の帳票すべてに、同じ修正が自動で行き渡る」

「なるほど……17枚の設計図を直すんじゃなくて、原本を直すんですね」

「やっと本題に追いついたかね、ワトソン君」

【After】必要なときに複製して差分だけ上書きする

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
my $incident = $registry->create(
    'incident_report',
    author   => '三橋彩香',
    date     => '2026-03-22',
    metadata => {
        division        => '運用部',
        approval_status => 'pending',
    },
);

my $minutes = $registry->create(
    'minutes',
    author => '三橋彩香',
);

「もう帳票ごとに長い new を書き直す必要はない。完成済みの原型から複製し、違うところだけ指定する。それだけだ」

解決:修正は原型だけ、事故は局所だけ

私は画面を見つめた。帳票を作るたびに全項目を列挙していたコードが、驚くほど静かになっている。

「しかもこれ、複製した後に incident_reportmetadata を変えても、minutes には影響しない……?」

「当然だ。dclone() で深く複製しているからね。もう隣の帳票にまでインクがにじむことはない」

ロックがテストを実行すると、ターミナルに美しい結果が並んだ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ prove -v t/01_prototype.t
# Subtest: Problem: Template Perfection Syndrome
    ok 1 - duplicated template definitions exist
    ok 2 - shallow copy leaks nested metadata
ok 1 - Problem: Template Perfection Syndrome
# Subtest: Solution: Prototype Pattern
    ok 1 - common fields come from base prototype
    ok 2 - create applies only the requested overrides
    ok 3 - cloned metadata is isolated from other documents
    ok 4 - unknown prototype is rejected
ok 2 - Solution: Prototype Pattern
All tests successful.

「見たまえ。Beforeのコードは修正箇所が全身に散らばり、浅いコピーで別の帳票まで巻き込む。Afterでは、原型の保管庫が変更点を一か所に閉じ込め、複製のたびに独立した完成品を渡している」

私は思わず息を吐いた。

「ずっと『丁寧に全部書くことが安全だ』と思っていました。でもそれ、同じ設計図を17回手で書き写していただけだったんですね……」

「その通り。君が守っていたのは秩序ではない。複製の手間という名の混沌だ」

私は立ち上がった。

「ありがとうございます、ロックさん! これで解決です! じゃあまず、Registryに17種類の原型を全部登録して、それから営業資料も稟議書も、全部この方式に載せ替えて……あと、昨日のBuilderも合わせて全部のクラスに入れて……」

「待ちたまえ!」

ロックの声が部屋に響いた。

「Prototypeは原型が安定していて、複製と差分適用が本質の場面で使うのだ。帳票のような『ひな形から量産するもの』には効く。だが、何でもかんでも複製前提で捉えるのは、コピー機を持った途端に全書類を複写し始める事務員と同じだぞ!」

私は返事をするより早くPCを閉じた。背後で何やら探偵が叫んでいたが、今の私には「原本を直せ」という言葉だけで十分だった。


探偵の調査報告書

容疑(アンチパターン)真実(パターン)証拠(効果)
テンプレ完璧症候群。似たようなオブジェクトを毎回 new 相当の処理でゼロから組み立て、共通定義が複数箇所に散在している状態。応急処置として浅いコピーを使うと、ネストした構造が参照共有されて別インスタンスまで汚染する。Prototype パターン。完成済みの原型を保管し、必要なときに複製して差分だけ適用する設計方式。深い複製を使えば、ネストしたデータ構造も安全に独立させられる。共通項目の変更が原型側の1箇所に集約された。生成コードは差分指定だけになり、意図が明確になった。浅いコピーによる参照共有事故も構造的に防止できるようになった。

推理のステップ

  1. 原型候補を見つける: 似たようなオブジェクトを何度も作っていて、共通項目と差分項目が分離できるかを確認する。
  2. 原型を保管する: 共通部分をPrototypeとしてまとめ、名前付きで取り出せるようにRegistryや専用クラスに閉じ込める。
  3. 深い複製を使う: ネストしたハッシュや配列を持つなら、浅いコピーではなく dclone() などで完全に独立した複製を作る。
  4. 差分だけ上書きする: 利用側は必要な変更点だけを指定し、原型の構造や共通項目には触れないようにする。
  5. 修正箇所を原型に寄せる: 新しい共通項目の追加や既定値の変更は、呼び出し側ではなく原型側で吸収する。

ロックより

ワトソン君。Prototypeは「コピペを正当化するパターン」ではない。むしろ逆だ。場当たり的なコピペをやめ、原本を管理したうえで、制御された複製だけを許可する仕組みなのだよ。

Builderが「正しく組み立てる門番」なら、Prototypeは「検証済みの完成品を安全に複製する保管庫」だ。毎回ゼロから作る必要がないものまで職人芸で組み上げていては、やがて修正が全身に転移する。

ただし、原型そのものが不安定だったり、複製よりも組み立て過程の制御が重要だったりするなら、Prototypeより別の道具が向いている。大事なのは、コピー機を手に入れたからといって、世界のすべてを複写し始めないことだ。

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