Featured image of post コードドクター【Composite】骨格溶解型フラット化症候群〜レシピツリーの再建術〜

コードドクター【Composite】骨格溶解型フラット化症候群〜レシピツリーの再建術〜

往診

料理は段取りが9割だ。

10年間、それを信じてフレンチの厨房に立ってきた。仕込み、ソース、メイン調理、盛り付け——頭の中のツリー構造を辿れば、何品だろうが同時に回せる。

だから、コードでも同じことができると思っていた。

俺はバンダナを外して首にかけ、モニタの前でうなだれた。ワンルームのデスクの横にはまな板とガスコンロ。壁には付箋だらけのレシピメモ。その隣のモニタには——真っ赤なエラーメッセージ。

レシピ管理アプリ。料理人のためのツール。俺が元料理人として、転職1年目の集大成として作っている個人開発アプリだ。

さっきまで動いていた。「デミグラスソース」をサブレシピとして追加したら、材料の集計がぶっ壊れた。バターが0グラムになってる。ありえない。バターの入ってないルーなんて存在しない。

インターホンが鳴った。

こんな時間に誰だ。バンダナを巻き直して、玄関を開けた。

長身の男が立っていた。黒い鞄を片手に、無表情で俺の顔を見ている。その横に、白い上着を羽織った女性が微笑んでいた。

「……ウーバーの方ですか?」

その黒い鞄が、どう見ても出前の保温バッグに見えたのだ。高級なやつの。

白衣の女性が、穏やかに首を振った。

「大丈夫ですよ、コード診療所の往診です。私はナナコ、助手をしています」

コード診療所? 往診? 頭が追いつく前に、先生と呼ばれた男はもう靴を脱いで上がり込んでいた。迷いのない足取りでデスクの前に立ち、モニタを覗き込んでいる。

「ちょ——勝手に入られても——」

先生がスクロールを止めた。画面には @recipes という配列——俺のレシピデータが全部入ったフラットなリスト。

先生の視線が、そこから壁の付箋に移った。ビーフシチューのレシピメモ。「ルー作り→バター30g、薄力粉30g」と書いた付箋が、「ビーフシチュー」と書かれた大きな付箋の中にぶら下がっている。ツリー状に。

先生は画面と付箋を3往復見比べた。

「……骨がない。」

触診

「骨?」

先生がモニタを指差した。画面をスクロールし、 $parent_id と書かれたフィールドの前で止まる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
my @recipes = (
    {
        id        => 1,
        parent_id => undef,
        type      => 'recipe',
        name      => 'ビーフシチュー',
    },
    {
        id        => 2,
        parent_id => 1,
        type      => 'recipe',
        name      => 'ルー作り',
    },
    {
        id        => 3,
        parent_id => 2,
        type      => 'ingredient',
        name      => 'バター',
        quantity  => 30,
        unit      => 'g',
    },
    # ... 全部フラットに並んでいる ...
);

「平ら。全部。」

ナナコさんが俺のほうを向いた。

「タケルさん、料理で言うとですね——仕込み用の材料も、ソースの材料も、メインの肉も、全部同じバットにバラバラに放り込んでいる状態です。作るたびに『この玉ねぎはシチュー用? カレー用?』って仕分けし直しているんですよ」

胸がズキッとした。料理人としてありえない。そんなことをしたら厨房が崩壊する。

「い、いや、parent_id でちゃんと紐づけてますよ! フラットにしたほうが管理しやすいかなって——」

先生はさらにスクロールした。材料を集計する関数の前で指が止まる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
sub calculate_total_ingredients ($recipe_id) {
    my %totals;
    for my $item (@recipes) {
        next unless defined $item->{parent_id};
        next unless $item->{parent_id} == $recipe_id;

        if ($item->{type} eq 'ingredient') {
            my $key = $item->{name};
            $totals{$key} //= { quantity => 0, unit => $item->{unit} };
            $totals{$key}{quantity} += $item->{quantity};
        }
        elsif ($item->{type} eq 'recipe') {
            # TODO: 3階層以上で壊れるかも? → 壊れた
            my $sub_totals = calculate_total_ingredients($item->{id});
            for my $name (keys %$sub_totals) {
                $totals{$name} //= { quantity => 0, unit => $sub_totals->{$name}{unit} };
                $totals{$name}{quantity} += $sub_totals->{$name}{quantity};
            }
        }
    }
    return \%totals;
}

「……毎回、組み立て直してる。」

ナナコさんが言い換えた。

「レシピのツリー構造が 骨格 だとすると、タケルさんのコードは骨が溶けてしまっているんです。臓器——材料データ——がフラットに散乱していて、使うたびに毎回外部から骨を組み立て直している状態ですね」

「骨が溶けてる……」

「はい。しかも type フィールドで材料なのかサブレシピなのかを毎回判定していますよね。自分が何者かを、自分で知らないんです」

先生が短く宣告した。

「診断。骨格溶解型フラット化症候群。」

外科手術

先生がデスクの前に座った。HHKB——小さな黒いキーボードをバッグから取り出して、俺のラップトップに繋ぐ。

ナナコさんが壁のレシピ付箋を指した。

「タケルさん、ここに貼ってあるレシピメモを見てください。ビーフシチューの下に、ルー作りがぶら下がっていますよね。さらにその下にバターと薄力粉。 ツリー になっています」

「そりゃそうだ。段取りってそういうもんだろ? 上から順に辿れば全工程がわかるように——」

「その ツリー構造 を、コードでもそのまま表現しましょう」

先生のキーの音が始まった。乾いた、リズミカルな打鍵音。

ナナコさんが画面を追いながら解説した。

「まず、材料もサブレシピも共通で持つ 骨格 を定義します。名前を返すことと、calculate() で材料を集計できること——それだけのシンプルな約束事です」

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package RecipeComponent;
use v5.36;
use Carp qw(croak);

sub new ($class, %args) {
    croak "name is required" unless $args{name};
    return bless { name => $args{name} }, $class;
}

sub name ($self) { $self->{name} }

sub calculate ($self) { croak ref($self) . "::calculate not implemented" }
sub display   ($self, $indent = 0) { croak ref($self) . "::display not implemented" }

1;

「次に、材料です。単品の材料は 葉っぱ ですね。ツリーの末端。自分の量を返すだけです」

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package Ingredient;
use v5.36;
use parent 'RecipeComponent';

sub new ($class, %args) {
    my $self = $class->SUPER::new(%args);
    $self->{quantity} = $args{quantity} // 0;
    $self->{unit}     = $args{unit}     // 'g';
    return $self;
}

sub quantity ($self) { $self->{quantity} }
sub unit     ($self) { $self->{unit} }

sub calculate ($self) {
    return { $self->name => { quantity => $self->{quantity}, unit => $self->{unit} } };
}

sub display ($self, $indent = 0) {
    my $prefix = '  ' x $indent;
    say "${prefix}${\$self->name}: $self->{quantity}$self->{unit}";
}

1;

「え、これだけ? バター1個が、自分で30gって知ってるだけ?」

先生が画面から目を離さずに答えた。

「……単一責務。正しい。」

ナナコさんが続けた。

「そして、ここからが Composite の核心です。レシピ——つまり ですね。材料も、サブレシピも、同じように子供として持てるようにします」

 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
package Recipe;
use v5.36;
use parent 'RecipeComponent';

sub new ($class, %args) {
    my $self = $class->SUPER::new(%args);
    $self->{children} = [];
    return $self;
}

sub add ($self, $child) {
    push $self->{children}->@*, $child;
    return $self;
}

sub calculate ($self) {
    my %totals;
    for my $child ($self->{children}->@*) {
        my $child_totals = $child->calculate;
        for my $name (keys %$child_totals) {
            $totals{$name} //= { quantity => 0, unit => $child_totals->{$name}{unit} };
            $totals{$name}{quantity} += $child_totals->{$name}{quantity};
        }
    }
    return \%totals;
}

sub display ($self, $indent = 0) {
    my $prefix = '  ' x $indent;
    say "${prefix}[${\$self->name}]";
    for my $child ($self->{children}->@*) {
        $child->display($indent + 1);
    }
}

1;

俺はモニタを見つめた。$child->calculate のところを。材料でもサブレシピでも、同じ calculate を呼んでいる。type のチェックも、ref() の分岐もない。

ナナコが壁のレシピ付箋を指しながら解説し、先生がコードを書き、タケルがコードと付箋の構造の一致に気づく瞬間

「待ってくれ」

声が出ていた。

「これって——俺が厨房で頭の中にやってることと、まったく同じ構造じゃないか」

ナナコさんが微笑んだ。

「そうなんです。『ビーフシチュー』の下に『ルー作り』があって、その下に『バター30g』がある。料理の段取り表の構造を、 そのまま コードにしたんですよ」

「仕込みの中に仕込みがあって、一番下に材料がある。上から順に合計すれば全材料がわかる——料理ならこう考えるのに、なんでコードだとフラットにしちまったんだ俺は」

先生が小さく頷いたように見えた。

術後経過

先生がキーボードから手を離した。画面にはテスト結果——全部緑。

「……試せ。」

俺はキーボードの前に座り直した。デミグラスソースのサブレシピを追加してみる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
my $roux = Recipe->new(name => 'ルー作り');
$roux->add(Ingredient->new(name => 'バター',  quantity => 30, unit => 'g'));
$roux->add(Ingredient->new(name => '薄力粉', quantity => 30, unit => 'g'));

my $demiglace = Recipe->new(name => 'デミグラスソース');
$demiglace->add(Ingredient->new(name => 'トマトペースト', quantity => 50, unit => 'g'));
$demiglace->add(Ingredient->new(name => '赤ワイン', quantity => 100, unit => 'ml'));

my $stew = Recipe->new(name => 'ビーフシチュー');
$stew->add($roux);
$stew->add($demiglace);
$stew->add(Ingredient->new(name => '牛すね肉', quantity => 400, unit => 'g'));
$stew->add(Ingredient->new(name => '玉ねぎ',   quantity => 200, unit => 'g'));
$stew->add(Ingredient->new(name => 'にんじん', quantity => 150, unit => 'g'));

$stew->calculate を実行する。バター30g、薄力粉30g、トマトペースト50g、赤ワイン100ml、牛すね肉400g、玉ねぎ200g、にんじん150g——全部正しい。

さっき壊れていた集計が、1行で動いている。

「サブレシピを追加しても、既存のコードは1行も変更してないです……」

$stew->display を実行する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[ビーフシチュー]
  [ルー作り]
    バター: 30g
    薄力粉: 30g
  [デミグラスソース]
    トマトペースト: 50g
    赤ワイン: 100ml
  牛すね肉: 400g
  玉ねぎ: 200g
  にんじん: 150g

壁に貼ってあるレシピメモの付箋と——まったく同じ形だ。

俺は冷蔵庫に向かった。

「先生、これ食ってくれませんか」

自家製プリンを出した。余った卵と砂糖で仕込んだやつだ。料理人の習性で、冷蔵庫には常に何か仕込んである。

先生がプリンを受け取った。一口。動きが止まった。目が微かに見開かれるのを、俺は見逃さなかった。そしてじっと俺の顔を見る。

その目は——厨房で料理長がコース料理の最終チェックをするときの目に似ていた。 認められた のか。料理人としての腕を。転職しても、この手が作るものに価値があるって——

「先生は甘いものが好きなだけですよ」

ナナコさんが落ち着いた声で言った。先生は何も言わず、もう一口食べている。

……そうか。

先生が帰り支度を始めた。鞄を持ち上げ、玄関に向かう。振り返らず、一言。

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

「先生——」

俺はまだ胸の奥が熱かった。

「俺、厨房で10年やってきたことが、コードで再現できるって、今日初めてわかりました。テスト書きます。誓って」

先生が小さく頷いた。少なくとも、そう見えた。

ナナコさんがドアの前で振り返った。

「タケルさん、料理の段取りをコードに活かせるエンジニアって、きっとすごく強いですよ。いつでもご連絡くださいね」

ドアが閉まった。

俺はしばらくその場に立っていた。それからモニタに向き直り、 $stew->display をもう一度実行した。

画面に表示されたレシピツリーは、俺が厨房で頭の中に描いていた段取り表と、まったく同じ形をしていた。


処方箋まとめ

症状適用すべき経過観察
ツリー構造をフラットな配列で管理し、parent_idで再構築している
葉ノード(末端要素)と複合ノード(コンテナ)を typeref() で毎回判定している
階層が深くなるたびに処理ロジックが壊れる・修正が必要になる
部分と全体を統一的に操作したい(再帰的な集計・表示)
階層が固定で2段以上にならないことが保証されている
ツリー構造の操作がなく、単なる一覧表示のみ

治療のステップ

  1. 共通骨格の定義 — すべてのノード(材料もレシピも)が共通で持つインターフェース RecipeComponent を定義する。calculate()display() を約束事として宣言
  2. 葉(Leaf)の実装 — 末端要素 Ingredient を実装。calculate() は自分自身の情報を返すだけ
  3. 枝(Composite)の実装 — コンテナ要素 Recipe を実装。子要素を持ち、calculate() は子に再帰的に委譲して集計
  4. 型チェックの排除type フィールドや ref() による分岐を撤廃。統一インターフェースによるポリモーフィズムで呼び出し
  5. ツリー構造の構築add() で葉も枝も同じように追加。構造変更が既存コードに影響しないことを確認

助手より

料理の段取りとCompositeパターンの構造は、本当にそっくりですね。「仕込みの中に仕込みがあって、一番下に材料がある」——タケルさんが10年間、頭の中で組み立ててきたツリーは、そのままコードの設計として通用するものでした。

先生はおそらくこう言うでしょう——「構造。正しい」。短いですけど、それが最大の賛辞だと思ってください。

——ナナコ

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