Featured image of post コードシェフの仕込み帳【Prototype】秘伝 of ベーススープ〜newのたびにかかるコストをcloneで解決する〜

コードシェフの仕込み帳【Prototype】秘伝 of ベーススープ〜newのたびにかかるコストをcloneで解決する〜

同じ設定を持つオブジェクトを毎回 new で生成し、初期化コストが跳ね上がる問題。PrototypeパターンをPerlとMooで実装し、深いコピー(Storable::dclone)とMooのwriterを駆使した安全な複製設計を解説します。

初夏の心地よい風が、古い木製の引き戸の隙間からすうっと吹き抜ける。 コード食堂の厨房では、私が「基本の和風出汁」を大鍋で引いている最中だった。昆布と鰹節の豊かな香りが、湯気とともに厨房いっぱいに広がっていく。 換気扇が静かに唸り、鍋がコトコトと音を立てる。私はお玉でアクを掬いながら、今日の仕込みの段取りを頭の中で描いていた。

「いい香りだな。見習い、火が強すぎるぞ。対流で出汁が濁る」 シェフが静かに包丁を置き、振り返らずに言った。 「あ、すみません!」 私は慌ててガスコンロのツマミを回し、弱火に落とした。シェフは無言で頷き、再びまな板の上でネギを刻み始めた。トントンと、小気味よい音が響く。

そのとき、店の古い引き戸が勢いよく開き、一人の男性が駆け込んできた。 「す、すみません……! ネットで見た、コード食堂というのはここですか?」 額に汗を浮かべ、焦りきった表情で息を切らしているのは、宮本さんと名乗る32歳の男性だった。 彼は「スパイス・ゲート」という人気のスープカレー店を営む店主で、頑固そうな眉毛と頑丈な体つきをしているが、今はひどく取り乱しているようだった。胸には大切なレシピやラップトップを抱え込んでいる。

「注文が重なると、調理が完全に詰まってしまうんだ。オーダーを受けてから秘伝のスパイスベースを毎回一から調合して煮込んでいると、どうやってもお客さんを待たせてしまう。プログラムでも同じことが起きていて、注文のたびにサーバーの負荷が跳ね上がって、お客さんの画面が固まるんだよ……!」 宮本さんはカウンターに滑り込むように座り、ラップトップを開いて画面をこちらに向けた。 「頼む、シェフ。俺の『まずいコード』を仕込み直してくれ!」

画面には、注文のたびにスパイスの配合比率の計算や、長時間の煮込み(重いデータベースアクセスやマスタ取得)を実行する、重いコンストラクタのコードが並んでいた。

この記事で学ぶこと

この記事は、コンストラクタ(new)で重い初期化処理を毎回繰り返す「コンストラクタ・オーバーヘッド」の問題を、Prototypeパターンを使って解決する話です。完成済みのインスタンスをメモリ上で複製(clone)し、個別に必要な箇所だけを調整して高速に配膳する設計手法を解説します。

学ぶことひとことで言うと
Prototype パターン既存のオブジェクト(原型)をコピー(クローン)して新しいオブジェクトを作成する生成パターン。ゼロからの初期化コストを回避する。
constructor-overheadnew の実行時に毎回 DB クエリや複雑な初期化ロジックが走り、オブジェクト生成がパフォーマンスのボトルネックになる問題。
Storable::dclonePerl標準モジュールによる「深いコピー(Deep Copy)」。ネストした配列やハッシュの参照共有バグを防ぎ、完全に独立したクローンを作成する。
Mooの writer の活用Mooの読み取り専用(is => 'ro')属性を、クローン時のみ安全にオーバーライドするため、プライベートセッターを利用した設計。

💡 MooとBUILDメソッドについて

  • Moo は、Perlでクラスや属性(メンバ変数)を簡潔に定義するためのオブジェクト指向用の軽量モジュールです。
  • BUILD はMooの特別なメソッドで、インスタンス生成(new)の直後に自動的に呼び出され、初期化やバリデーションを行うために使われます。

対象読者は、次のような人を想定しています。

  • PerlとMooの基本的な使い方(has, new, BUILD)を理解している
  • ループ内や大量リクエスト時に new を繰り返した結果、パフォーマンスの低下に悩んだことがある
  • 「浅いコピー」と「深いコピー」の違いや、Perlの参照の罠について学びたい

毎回ゼロからスパイスを挽いて煮込むコード

シェフはストーブの前で手を温めながら、宮本さんのラップトップの前に歩み寄った。 画面に書かれていたのは、スープカレーのベーススープを表現する CurryBase クラスと、注文を処理するループのコードだった。

 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
package CurryBase;
use Moo; # オブジェクト指向を簡潔に記述する軽量モジュール
use v5.36;
use Time::HiRes qw(sleep);

has base_soup => (is => 'ro', required => 1);
has spiciness => (is => 'rw', default => 3);
has toppings  => (is => 'rw', default => sub { ['チキン', 'ニンジン', 'ジャガイモ'] });

sub BUILD ($self, $args) {
    # スパイスの焙煎、玉ねぎの飴色炒めなどの重い初期化をシミュレート
    Time::HiRes::sleep(0.1); # 100msの重い処理
}

package main;
use v5.36;
use Time::HiRes qw(gettimeofday tv_interval);

# 注文のたびに毎回ゼロから new する(Before)
my $t0 = [gettimeofday];
for (1..10) {
    my $curry = CurryBase->new(
        base_soup => '秘伝スープ',
        spiciness => 5, # 注文ごとに辛さを調整する
    );
}
my $elapsed = tv_interval($t0);
say "Before 実行時間: ${elapsed}秒"; # 約1秒かかってしまう

「動いてはいるんだ。でも、バッチ処理で何千個もカレーを配膳するときや、ランチタイムのリクエスト集中時に、この100msの遅延がどんどん積み重なってサーバーが詰まってしまう」 宮本さんはもどかしそうに頭を抱えた。 「基本のスープや具材は9割以上同じなのに、毎回 new でゼロから作るしかなくて……」

私は画面のコードを見つめ、前々回の Bridge(トッピングとベースの分離)や前回の Visitor(データと処理の分離)で学んだことを頭の中で結びつけた。 「あ、これ! 毎回ゼロから new して重い初期化を走らせるんじゃなくて、最初に作った設定オブジェクトをコピーして使えばいいんじゃないですか? ……確か、Prototype っていうパターンですよね!」 私は自分のひらめきに少し得意げになって、自信満々にシェフに提案した。

シェフは私の顔をじっと見つめ、少しだけ口角を上げた。 「ほう、見習い。言うようになったな」 しかし、すぐに表情を引き締め、厳しい声で続けた。 「だが、ただコピーすればいいというものではないぞ。Perlの仕込みで生煮えのままクローンを作ると、客全員のカレーが台無しになる。鼻を折る前に、まず問題の本質を整理しろ」


浅いコピーの罠:トッピング皿の共有

シェフは厨房のテーブルをトントンと指で叩いた。 「宮本さん、あんたのコードは、注文が入るたびに毎回、水から昆布と鰹節を煮出して一からスープのベースを作っているようなものだ。しかも、その出汁を一瞬で引こうとするから、コンロの火力を最大にしてCPUが悲鳴を上げている」 「そうなんです! だから事前に仕込んだスープを使いたいんですけど……」

「仕込み自体(ベースのスープ)は完璧だ。だが、配る段取りが壊れてる」 シェフは私の顔を見た。 「見習い。お前が得意顔で言った『ただコピーする』というコードを、宮本さんのスープで試してみよう。宮本さん、カレーのボウルが1つあるとする。それをハッシュの展開で単純にコピーしたらどうなる?」

1
2
3
4
5
6
7
8
9
# 浅いコピー(Shallow Copy)のシミュレート
my $prototype = CurryBase->new(
    base_soup => '秘伝スープ',
);

# 元のオブジェクト(ハッシュリファレンス)をデリファレンスして新しいハッシュを作成し、
# 再度同じパッケージ名で bless(オブジェクト化)することで、浅いコピーをシミュレートする
my $shallow_cloned = { %$prototype };
bless $shallow_cloned, ref($prototype);

「ボウルを2つに分けたので、片方の辛さを変えて、トッピングを追加してみます!」 私は意気揚々とコードを動かした。

1
2
3
4
5
# クローン側の辛さを5にする
$shallow_cloned->spiciness(5);

# 配列リファレンスを @{ ... } でデリファレンスして要素を追加
push @{ $shallow_cloned->toppings }, 'チーズ';

すると、画面に出力された元の $prototype(プロトタイプ)のトッピング情報を見て、私は言葉を失った。 「あれ……? 元のプロトタイプのトッピングリストにも『チーズ』が追加されてる……! 辛さは3のままで無事なのに、トッピングだけが勝手に増えちゃってます!」

シェフは we しくため息をついた。 「これが浅いコピー(Shallow Copy)の罠だ。ハッシュの第1階層(辛さの数値など)はコピーされても、第2階層の『トッピングの皿(配列リファレンス)』はコピーされず、トッピング皿が1つしかなくて共有されたままになるぞ」 「トッピング皿が、元のカレーとクローンのカレーで同じ皿を使っていた……?」

「そうだ。同じ皿を共有しているから、クローンしたカレーに『チーズ追加!』と載せたつもりが、元のプロトタイプスープの具材皿にもチーズが入ってしまう。これでは、後から来る別のお客さんのカレーすべてに、頼んでもいないチーズが自動的に入り込んでしまうバグになるぞ。Perlの参照(ポインタ)を見落とす料理人は、厨房に立つ資格はない」 私は自分の浅はかさに赤面し、小さく身を縮めた。「あたたっ……すみません……」

宮本さんは目を見開いた。 「そうか……! 共有されてしまうから、メモリ上での単純コピーは危険だと考えて new を繰り返していたんだ。でも、これじゃ完全に独立した『深いコピー』はどうやって作ればいいんだ?」

「仕込みを直すぞ」 シェフは静かに大鍋の火を止めた。


仕込み直し:Storable::dclone と Moo の writer

シェフはホワイトボードに、深いコピー(Deep Copy)の設計図を描き始めた。

「Perlの参照の繋がりをすべて断ち切り、ネストした配列やハッシュまで完全に新しい皿として複製するには、**深いコピー(Deep Copy)**が必要だ。Perlにはそのための標準モジュール Storabledclone という強力な道具がある」

1
2
3
4
use Storable qw(dclone);

# $self を丸ごと複製する。ネストした配列リファレンスも別のアドレスに配置される
my $cloned = dclone($self);

「これで、トッピング皿も完全に別の皿として複製されるため、互いに干渉しなくなる」

さらに、シェフはMooの属性定義のコードを書き換えた。 「もう一つの仕込みだ。Mooではオブジェクトの安全性を保つために、変更されたくない属性を is => 'ro'(読み取り専用)にするのがベストプラクティスだ。だが、クローンするときだけは、ベースのスープを変更したい場合もある。どうする?」

宮本さんは首を振った。 「直接ハッシュのキー($cloned->{base_soup})を書き換えるコードは見たことがありますが、それはMooの型制約(isa)やトリガーをバイパスしてしまって危険ですよね?」

「その通りだ。直接ハッシュを書き換えてしまうと、Mooが提供する型チェック(例えば『文字列しか受け付けない属性に数値を設定してしまう』など)や、値が変わった時に連動して動く処理(トリガー)が一切実行されず、後で予期せぬエラーを引き起こす原因になる」 シェフは頷いた。 「だから、Mooの writer オプションを活用する。クローン時に安全に上書きするための専用の口(プライベートセッター)を仕込んでおくんだ」

 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
# CurryBase.pm (Afterコード)
package CurryBase;
use Moo;
use v5.36;
use Storable qw(dclone);
use Time::HiRes ();
use Carp; # エラーハンドリングのための標準モジュール

# is => 'ro' の属性は、クローン時に安全に上書きできるよう内部用セッター (writer) を定義
# writer を指定すると、is => 'ro' のままであってもこのメソッド経由で安全に値を書き換えられます
has base_soup => (is => 'ro', required => 1, writer => '_set_base_soup');
has spiciness => (is => 'rw', default => 3);
has toppings  => (is => 'rw', default => sub { ['チキン', 'ニンジン', 'ジャガイモ'] });

sub BUILD ($self, $args) {
    # 重い初期化(プロトタイプ生成の1回だけ実行される)
    Time::HiRes::sleep(0.1);
}

# Prototypeパターンの本質: clone メソッド
sub clone ($self, %override) {
    # Storable::dclone を使って、完全に独立した深いコピーを作成
    my $cloned = dclone($self);

    # 個別のカスタマイズ(上書き)を安全に適用
    for my $key (keys %override) {
        # is => 'ro' 属性の上書き用(プライベートセッター _set_属性名 が存在すれば呼ぶ)
        # can() はオブジェクトがそのメソッドを持っているか調べるPerlの標準機能
        if (my $writer = $cloned->can("_set_$key")) {
            $cloned->$writer($override{$key});
        }
        # is => 'rw' 属性の上書き用(通常のセッターが存在すれば呼ぶ)
        elsif ($cloned->can($key)) {
            $cloned->$key($override{$key});
        }
        else {
            Carp::croak("Unknown attribute: $key");
        }
    }
    return $cloned;
}

宮本さんは食い入るようにコードを見つめ、疑問を口にした。 「深いコピーは安全ですが、dclone 自体のコストは重くないですか? new で初期化するのと比べて、本当に速くなるんでしょうか?」

「良い問いだ、宮本さん」 シェフは宮本さんの目を見て、力強く断定した。 「dclone はメモリ上でのシリアライズとデシリアライズを伴うから、確かにタダではない。だがな、データベースにクエリを投げたり、外部APIを叩いたりするI/Oのコストに比べれば、メモリ上での複製コストなど微々たるものだ。実際に計測してみろ。桁が違う」

私はすかさず、シェフの言葉を自分の言葉で翻訳した。 「つまり、注文のたびにスパイスを調合して一からスープを煮出す(I/O)より、仕込み終わったベーススープを小鍋に取り分ける(クローン)方が、圧倒的に早いということですね!」 シェフは小さく頷いた。 「まあ、そういうことだ。仕込み終わったものを複製するだけで、生み出すスピードは劇的に上がる」

💡 Storable::dclone の注意点と限界

Storable::dclone はメモリ上のリファレンスの繋がりを丸ごとシリアライズ(直列化)して複製する非常に便利な道具ですが、いくつかの重大な制約があります。

シリアライズできないデータ

  • コードリファレンス(CODE や、実行状態を持つ 正規表現オブジェクト(Regexp
  • データベース接続ハンドル(DBI::db など)やファイルハンドルなどの 外部リソースへの接続

もしオブジェクトの属性にこれらが含まれていると、dcloneRetrieve of CODE... not implementedCan't store ... といった実行時エラーを吐いてクラッシュします。

対策 Prototypeパターンを適用するオブジェクトには、外部接続などのアクティブなリソースを持たせず、I/Oの結果として得られた 純粋なデータ(ハッシュや配列、数値)のみを保持する設計 にすることが重要です。


試食合格:桁違いのスピード

私たちは宮本さんのラップトップで After コードを実行し、テストを走らせた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main;
use v5.36;
use Time::HiRes qw(gettimeofday tv_interval);

# 1. 最初にプロトタイプ(原型)を1回だけ重い初期化で仕込む
my $prototype = CurryBase->new(
    base_soup => '秘伝スープ',
);

# 2. 以降は clone して一部だけ上書きして配膳する(After)
my $t0 = [gettimeofday];
my @curries;
for (1..10) {
    push @curries, $prototype->clone(
        spiciness => 5,
    );
}
my $elapsed = tv_interval($t0);
say "After 実行時間: ${elapsed}秒";

コンソールに表示された結果を見て、宮本さんは息を呑んだ。

1
2
Before 実行時間: 1.002秒
After 実行時間: 0.0018秒

「1000倍近く速い……! 1秒以上かかっていた処理が、ミリ秒以下で終わっている!」 さらに、クローンしたカレーのトッピングを変更しても、元のプロトタイプのトッピングが一切汚染されていない(深いコピーの保証)ことも、テストコードで完璧に確認できた。

シェフは腕を組み、満足そうに頷いた。 「仕込みは一回。あとは複製して個別に味を整える。完成だ、配膳しろ」

宮本さんの表情から、焦りと緊張がすっと消え去った。 「すごい……。これなら、ピーク時に注文が殺到しても、サーバーが詰まることなく一瞬でレスポンスを返せます。何より、Mooの writer のおかげで安全に読み取り専用属性もクローン時にカスタマイズできるのが美しいですね!」 彼は晴れやかな笑顔を見せ、何度も深く頭を下げた。 「本当に助かりました。これで自信を持って店に戻れます!」

宮本さんは、ラップトップを大切にケースにしまい、コード食堂を後にした。彼の背中は、店に来たときとは打って変わって、自信に満ち溢れているように見えた。


シェフの仕込み帳

今回の料理(設計)のまとめ

Prototypeパターンのクラス構成図。CurryBase(Concrete Prototype)がCloneableロール(Prototype)を実装し、Clientがそのインスタンスをディープコピー(clone)して新しいオブジェクトを生成する関係を示しています。

料理(設計)の工程説明
本日の不味いコード(Before)同じベーススープ(初期化)を、注文(new)のたびに一から煮出す(コンストラクタ内で重いDB取得やI/Oを繰り返し、パフォーマンスが低下する)
秘伝の隠し味(原因分析)初期設定が9割以上共通しているのに、ゼロからの new に固執している。また、安易なハッシュコピー(浅いコピー)では配列などの参照が共有され、データが汚染されてしまう
プロの仕込み(After)Prototypeパターンの導入。最初に1回だけプロトタイプを生成し、以降は Storable::dclone で深いコピーをして複製。Mooの writer オプションで読み取り専用属性も安全に個別調整する
今回の試食結果(効果)10回 new するのに約 1.0 秒かかっていた処理が、クローン化によって数ミリ秒以下へ短縮。深いコピーによりトッピングが干渉するバグも完全に解消

再現手順

あなたが書いたコードで、同じ設定のオブジェクトの new が繰り返され、初期化コストがボトルネックになっている場合は、以下のステップで仕込み直してください。

  1. プロトタイプの定義
    • is => 'ro'(読み取り専用)の属性には、クローン時のみ上書きできるように writer オプション(例: writer => '_set_attr_name')を付与する
  2. clone メソッドの実装
    • クラスに clone メソッドを実装し、内部で Storable::dclone を用いてオブジェクト自身のディープコピーを作成する
    • clone メソッドの引数 %override をループで回し、内部セッターまたは通常のセッターが存在する場合のみ属性値を上書きする
  3. プロトタイプの事前作成とクローン利用
    • アプリケーション起動時や処理の開始時に、原型(プロトタイプ)となるインスタンスを一度だけ生成する
    • 以降は、その原型から $prototype->clone(...) を呼び出してインスタンスを複製する

「見習い、お前が Prototype を自分で選んだのは悪くない。仕込みの引き出しが増えたのは認めよう」 シェフが、大鍋に残った和風出汁を小さな器に注ぎ、私に手渡しながら言った。 「えっ、本当ですか!」 私は嬉しくなって器を受け取った。

「だがな」シェフの目が、少しだけ悪戯っぽく細められる。 「複製した後の“中身の繋がり(ポインタ)”まで目を行き届かせるのが、プロの仕事だ。ただ型を真似しただけの料理は、客の腹を壊す。まずはこの出汁を飲んで、お前が引いた出汁と、俺の引いた出汁の違いを頭にクローンしておけ」

出汁を一口啜ると、雑味が一切ない、深い旨味がじんわりと体に染み渡った。 「……全然違います。私の出汁には、まだ迷いがある気がします」 「焦るな。仕込みは一日にして成らずだ」

シェフはそう言うと、再び静かに包丁を握り直した。


[Model: Gemini 3.5 Flash (High)]

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