Featured image of post コードドクター【Flyweight】クローン増殖症候群〜居酒屋カウンターの仕入れ台帳革命〜

コードドクター【Flyweight】クローン増殖症候群〜居酒屋カウンターの仕入れ台帳革命〜

往診

金曜の夜が怖い。

飲食店を経営している人間がこんなことを言うと笑われるかもしれないが、俺にとっての金曜日は「書き入れ時」であると同時に「システムが死ぬ日」でもあった。

俺は河野大輝、38歳。「炭火 だいき」という居酒屋を3店舗やっている。炭火焼き鳥と日本酒がウリの、そこそこ繁盛している店だ。原価率は28%に抑えている。仕入れの管理はきっちりやるタイプだ。

問題は注文管理システムだった。

外注すると月額利用料がバカにならない。だから3年前に独学でPerlを覚えて自作した。注文を取る、集計する、レジで精算する。これだけのシステムなのに、金曜の夜になると画面がフリーズする。端末を再起動すれば復活するが、ピーク時のクレームは胃に来る。

あの日も仕込みをしていた。焼き鳥を串に刺しながら「今週もまた落ちるのか」と憂鬱な気分でいたところ、裏口のドアがノックされた。

「どちら様ですか?」

開けると、黒い鞄を持った無表情の男と、笑顔の女性が立っていた。

「大丈夫ですよ、ここはコード診療所です……あ、いえ、往診ですね」

女性——助手のナナコと名乗った——がそう言った。

「コード? 診療所? ……まさか営業じゃねえだろうな。うちは外注は使わねえ主義だ」

男の方はノーリアクションだった。俺の返事を聞いていたのかどうかもわからない。だが男はすでに俺の横を通り抜け、バックヤードのPCの前に座っていた。

「おい、勝手に触るな! それ俺のシステムだぞ!」

「先生に少しだけ見せていただけますか? 金曜日に調子が悪くなるとのこと、拝見したいんです」

ナナコさんがやんわりと言った。

怪しい。怪しすぎる。だが「金曜日に調子が悪くなる」という情報を知っていること、それに——正直に言えば、誰かにこの悩みを相談したかった。

渋々、俺はPCのロックを解除した。

触診

ドクターは画面をスクロールしていた。無言で、無表情で。俺が丹精込めて書いたコードを流し読みしている。

3分ほど経って、ドクターが初めて口を開いた。

「……増殖しすぎだ」

「増殖?」

血の気が引いた。もしかしてウイルスにでもやられたのか。顧客データが漏れていたら——

「いえ、セキュリティの問題ではないんです」

ナナコさんが慌てて補足した。

「メニューの情報……たとえば"生ビール 550円"というデータが、注文のたびに丸ごとコピーされているんです」

「当たり前だろ?」

俺は首をかしげた。テーブル1の注文とテーブル2の注文は 別モノ だ。テーブルが違えば、注文も別だ。

「客が違うんだから、注文データが別なのは当然だろ?」

ドクターが画面を指さした。

「ビールは……一種類だ」

短い一言だった。だが、何かが引っかかった。

ナナコさんが微笑んで続けた。

「河野さん、食材の仕入れで考えてみてください。生ビールを50テーブル分出すとき、仕入れ先の情報——メーカー名、仕入れ価格、ロット番号って、テーブルごとに別々に管理しますか?」

「……するわけねえだろ」

即答だった。仕入れ台帳は1つだ。各テーブルに必要なのは「何杯頼まれたか」だけだ。仕入れ先の情報を50回書き写す飲食店なんてない。

「それだ」

ドクターが頷いた。

沈黙が3秒ほど流れた。

「……待てよ」

俺は自分のコードを見直した。注文が入るたびに MenuItem->new(...) が呼ばれている。名前も価格もカロリーも画像パスも、全部その場で新しく作られている。

「俺のコード……テーブルごとにビールの仕入れ台帳まるごと作ってんのか?」

ナナコさんが静かに頷いた。

「50テーブルで各20品注文が入ると、1,000個のメニューオブジェクトが生まれます。そのほとんどが同じ内容の複製です」

1,000個。それが3店舗分で3,000個。金曜の夜、全店舗がフル稼働するとき——

「だから金曜に落ちてたのか……!」

仕入れ価格表とコードを見比べ、すべてを理解する河野

壁に貼ってある仕入れ価格表が目に入った。あの表は1枚だ。全テーブルの注文がこの1枚を参照している。それなのに、俺のコードでは——

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package Order;
use v5.36;
use MenuItem;

sub new ($class, %args) {
    # メニュー情報をまるごとコピーして注文オブジェクトに持たせる
    my $item = MenuItem->new(
        name     => $args{menu_name},
        price    => $args{menu_price},
        calorie  => $args{menu_calorie},
        category => $args{menu_category},
        image    => $args{menu_image},
    );

    return bless {
        menu_item  => $item,
        table_no   => $args{table_no},
        quantity   => $args{quantity},
        ordered_at => $args{ordered_at} // time(),
    }, $class;
}

毎回 MenuItem->new を呼んでいる。同じ生ビールなのに。

「原価率28%に抑えてるって自慢してたくせに、メモリの原価率は聞きたくねえな……」

自嘲気味に呟くと、ドクターが一瞬だけこちらを見た。なにか言いたそうだったが、結局何も言わなかった。

外科手術

ドクターが手術を始めた——と言っても、キーボードを叩き始めただけだが。

俺はその背中を見ながら、ナナコさんの解説を聞いていた。

「今先生がやっているのは、メニューの “マスター台帳” を作っているところです。仕入れ台帳と同じで、品目ごとに1枚だけですよ」

ドクターの指が止まった。画面に新しいクラスが表示されていた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package MenuItemPool;
use v5.36;
use MenuItem;

sub new ($class) {
    return bless { pool => {} }, $class;
}

sub get ($self, $name, %args) {
    unless (exists $self->{pool}{$name}) {
        $self->{pool}{$name} = MenuItem->new(name => $name, %args);
    }
    return $self->{pool}{$name};
}

sub update_price ($self, $name, $new_price) {
    die "メニュー '$name' はプールに存在しません"
        unless exists $self->{pool}{$name};
    $self->{pool}{$name}->set_price($new_price);
    return $self;
}

MenuItemPool ……メニューのプール?」

「はい。“生ビール"というメニューが初めて注文されたときだけオブジェクトを作って、2回目以降は同じものを返すんです」

なるほど。仕入れで言えば、最初の仕入れ時にマスターを登録して、あとはそのマスターを参照するだけ。ビールを1杯売るたびにメーカーに電話して品目情報を聞き直すバカはいない。

「では注文のほうも変わりますよ」

ナナコさんが画面を示した。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package Order;
use v5.36;

sub new ($class, %args) {
    return bless {
        menu_item  => $args{menu_item},   # 共有オブジェクトへの参照
        table_no   => $args{table_no},
        quantity   => $args{quantity},
        ordered_at => $args{ordered_at} // time(),
    }, $class;
}

Before では menu_name, menu_price, menu_calorie……と全部の情報を受け取っていたのが、After ではただ menu_item を受け取るだけだ。仕入れ台帳の"参照番号"を持つようなものだ。

「で、注文を追加するときはこうなります」

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# OrderSystem の add_order メソッド
sub add_order ($self, %args) {
    my $item = $self->{pool}->get(
        $args{menu_name},
        price    => $args{menu_price},
        calorie  => $args{menu_calorie},
        category => $args{menu_category},
    );

    my $order = Order->new(
        menu_item  => $item,
        table_no   => $args{table_no},
        quantity   => $args{quantity},
    );

    push $self->{orders}->@*, $order;
    return $order;
}

「プールに聞いて、あれば既存のを返す。なければ新しく作ってプールに入れる」

「その通りです。これで、どのテーブルから何杯注文が入っても、“生ビール"のオブジェクトは1つだけです」

ドクターが肩越しにこちらを見た。

「仕入れと……同じだ」

短い言葉だったが、俺にはそれで十分だった。

ここで、ちょっとした出来事があった。

手術が長引きそうだったので、仕込み中の焼き鳥を何本か温め直して出した。せっかく来てもらったんだ、これくらいはする。

ナナコさんが「ありがとうございます」と言って取り皿を出した。ドクターの分もだ。ドクターはコードを書く手を一瞬止め、ナナコさんのほうを見た。それから焼き鳥を一本つまみ、小さく頷いた。満足げに。焼き鳥の味に対してなのか、取り皿を出してもらったことに対してなのか、判然としなかった。

「すみません、先生は集中していると周りが見えなくなるんです」

ナナコさんが苦笑いしながら言った。

……何なんだ、この二人。

術後経過

ドクターが作業を終えた。

画面にメモリ使用量のグラフが表示されていた。Before と After、2本の折れ線。

Before は右肩上がりの急カーブ——注文が増えるたびにメニューオブジェクトが際限なく増殖し、金曜の夜にはメモリの天井にぶつかる。

After は、ほとんど水平な線だった。

「3,000 から……50 か」

思わず声に出た。ナナコさんが頷いた。

「メニューの種類の数だけしかオブジェクトは存在しません。注文が何千件来ても、プール内のメニュー数は変わりませんよ」

98%削減。食材のロス率でこんな数字を叩き出したら表彰ものだ。

「それと、もう一つ大きな恩恵があります」

ナナコさんが続けた。

「価格改定のとき、今までは全注文を走査して一つずつ書き換えていましたよね? プールを使えば、マスターの1箇所を変更するだけで、全注文に即座に反映されます」

「仕入れ価格表と同じだ……。あの壁の表を1枚書き換えれば、全テーブルの原価が変わる」

ドクターが立ち上がった。

「対価は……テストだ」

「え?」

「先生は金銭ではなく、コードの品質でお返しをいただくんです」

ナナコさんが補足した。

「今回でしたら、メニューの価格を変更したときにプールが正しく更新されるか、そのテストを書いていただければ」

テストか。まあ、仕入れ先を変えたときに在庫の棚卸しをするのと同じことだろう。

「……わかった。テストは書く」

ドクターは鞄を手に取り、裏口に向かった。ナナコさんが軽く会釈して続く。

「河野さん、金曜日の夜が楽しみになりますね」

ナナコさんの笑顔を見送りながら、俺はバックヤードのPCに向き直った。壁の仕入れ価格表が視界の端に映る。

仕入れ台帳は1つ。マスターは1つ。コードも、同じだったんだ。

「共有……共有か」

焼き鳥のタレの匂いが漂うバックヤードで、俺はテストコードを書き始めた。


処方箋まとめ

症状適用すべき経過観察
同一データのオブジェクトが大量に複製されている
メモリ使用量が扱うデータ件数に比例して増加する
マスターデータ変更時に全オブジェクトを走査して更新している
オブジェクトごとに固有の状態を持ち、共有できない
オブジェクト数が少なく、メモリ消費が問題にならない

治療のステップ

  1. 共通部分(Intrinsic State)の特定 — オブジェクトの中で「全インスタンスで同一」な属性を洗い出す(例: メニューの名前・価格・カロリー)
  2. 個別部分(Extrinsic State)の分離 — インスタンスごとに異なる属性を外部に切り出す(例: テーブル番号・注文数量・注文時刻)
  3. Flyweight クラスの作成 — Intrinsic State のみを保持する軽量オブジェクトを定義する
  4. Flyweight Factory(プール)の実装 — 同一キーに対して同一インスタンスを返すファクトリを作成する
  5. クライアントの修正 — オブジェクトを直接 new せず、ファクトリ経由で取得するように変更する
  6. テストの追加 — 同一キーで取得したオブジェクトが同一インスタンスであること、マスター変更が全参照に反映されることを検証する

助手より

河野さん、お疲れさまでした。お店の経営で培った「ムダを省く力」は、そのままコード設計にも活きるんですよ。仕入れ台帳を1つにまとめることと、Flyweight パターンでオブジェクトを共有すること——本質は同じです。

金曜の夜が「システムが落ちる日」から「一番の稼ぎ時」に戻ることを願っています。焼き鳥、とても美味しかったです。先生も満足そうでしたよ。

——ナナコ

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