「助けてください! 帳票テンプレートに項目を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() を使って深い複製を作っていることだ。これで metadata や sections のようなネストした構造も安全に独立する」
「じゃあ、帳票の共通部分は……?」
「最初から原型にまとめておくのだよ」
【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_report の metadata を変えても、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箇所に集約された。生成コードは差分指定だけになり、意図が明確になった。浅いコピーによる参照共有事故も構造的に防止できるようになった。 |
推理のステップ
- 原型候補を見つける: 似たようなオブジェクトを何度も作っていて、共通項目と差分項目が分離できるかを確認する。
- 原型を保管する: 共通部分をPrototypeとしてまとめ、名前付きで取り出せるようにRegistryや専用クラスに閉じ込める。
- 深い複製を使う: ネストしたハッシュや配列を持つなら、浅いコピーではなく
dclone() などで完全に独立した複製を作る。 - 差分だけ上書きする: 利用側は必要な変更点だけを指定し、原型の構造や共通項目には触れないようにする。
- 修正箇所を原型に寄せる: 新しい共通項目の追加や既定値の変更は、呼び出し側ではなく原型側で吸収する。
ロックより
ワトソン君。Prototypeは「コピペを正当化するパターン」ではない。むしろ逆だ。場当たり的なコピペをやめ、原本を管理したうえで、制御された複製だけを許可する仕組みなのだよ。
Builderが「正しく組み立てる門番」なら、Prototypeは「検証済みの完成品を安全に複製する保管庫」だ。毎回ゼロから作る必要がないものまで職人芸で組み上げていては、やがて修正が全身に転移する。
ただし、原型そのものが不安定だったり、複製よりも組み立て過程の制御が重要だったりするなら、Prototypeより別の道具が向いている。大事なのは、コピー機を手に入れたからといって、世界のすべてを複写し始めないことだ。