Featured image of post コードドクター【State】フラグ免疫不全症候群〜調理工程カンバンと状態オブジェクトの移植術〜

コードドクター【State】フラグ免疫不全症候群〜調理工程カンバンと状態オブジェクトの移植術〜

土曜日の昼下がり。俺はリビングのPCの前で、自分が書いたコードと格闘していた。

「麺マスター」——ラーメン一杯の調理工程を完全にコード化した、俺の自信作だ。注文受付、仕込み、茹で、盛り付け、提供。ラーメン屋を10年やってきた俺だからこそ書ける、現場の知恵が詰まったシステム。のはずだった。

「茹で中なのに盛り付け開始しちゃいました」

後輩の山本から、今朝もそんな報告が飛んできた。ラーメンで言えば、麺がまだ硬いのにどんぶりに入れちゃうようなもんだ。ありえないだろ? 俺の店だったら即クビだ。

俺の名前は麺田剛志。38歳。元ラーメン屋「剛志軒」の店主で、今はWebエンジニア3年目。ラーメン屋時代に原材料管理で苦労して、「だったら自分でシステム作ればいい」とプログラミングを始めた。今は中小SaaS企業で社内ツールを作りながら、週末は個人プロジェクト「麺マスター」を開発している。

同僚の田中がチャットで言ってきた。「麺田さん、知り合いにコードの医者がいるんですけど」。コードの医者? 胡散臭ぇ。でも3週間、このバグと格闘して一向に直らない。気合だけじゃどうにもならない壁に、俺はぶち当たっていた。

往診

インターホンが鳴った。

「どちら様ですか」

モニターに映ったのは、黒い鞄を持った無表情の長身の男と、笑顔の女性。怪しい。だが田中の紹介だ。

ドアを開けると、女性が先に口を開いた。

「大丈夫ですよ、ここはコード診療所です……あ、いえ、往診ですね。ナナコと申します」

「往診? 出前みたいなもんか」

俺の言葉にナナコさんは微笑みを浮かべるだけだった。隣の男——コードドクターと呼ばれているらしい——は無言で俺の部屋を見回した。壁に掛かった「剛志軒」の暖簾、台所の業務用寸胴鍋、棚に並んだ製麺の本。そしてPCデスク。

ドクターの目線が一瞬、寸胴鍋で止まった気がしたが、すぐにPCの方に向かった。

「まぁ、上がってくれ。コーヒーでいいか?」

ドクターは何も答えず、俺のPCデスクの横に椅子を引き寄せて座った。

「すみません、先生はこういう方なんです」

ナナコさんが申し訳なさそうに頭を下げる。俺は呆れたが、まぁ腕の良い料理人っていうのは得てして無口だ。そういうタイプかもしれない。

「麺マスターを見せてくれ、ってことか?」

俺はノートPCを開いて、コードを表示した。

「俺が作った調理管理システムなんだけどさ。ラーメン一杯作る工程を全部コードで管理してるんだ。仕込み、茹で、盛り付け、提供……完璧にモデル化したはず」

俺は胸を張った。

「でもよ、後輩に使わせたら『茹で中なのに盛り付け開始しちゃいました』ってバグが出てさ。ラーメンで言えば、麺がまだ硬いのにどんぶりに——」

「コード」

ドクターが初めて口を開いた。一言だけ。

「あ、はい……」

触診

ドクターの長い指が、マウスホイールをゆっくり回した。コードが画面を流れていく。そして——フラグ変数の宣言部分で、指が止まった。

1
2
3
4
5
6
# 工程フラグ(全部必要なんだよ!)
is_ready    => 0,  # 注文受付OK
is_boiling  => 0,  # 茹で中
is_topping  => 0,  # 盛り付け中
has_error   => 0,  # エラー発生
is_served   => 0,  # 提供済み

「……5本」

ドクターが呟いた。

ナナコさんが俺に向き直る。「5本のフラグで状態を管理されているんですね。これは……体内に5本の信号線が独立して走っているような状態です」

「5本? ラーメンの基本要素は5つだろ? 麺、スープ、チャーシュー、ネギ、メンマ……完璧じゃねぇか」

「……技術的には完璧とは少し違うかもしれません」

ナナコさんは苦笑しつつ、ドクターの方を見た。ドクターはもう次のコードをスクロールしている。処理関数の中身だ。

 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
# 茹で開始
sub start_boiling($self) {
    # 注文受けてないと茹でられない
    if (!$self->{is_ready}) {
        die "まだ注文来てないぞ!\n";
    }
    # エラー中は無理
    if ($self->{has_error}) {
        die "エラー中!\n";
    }
    # もう茹でてたら二重茹でになる
    if ($self->{is_boiling}) {
        die "もう茹でてるって!\n";
    }
    # 盛り付け中に茹で直し? ありえない
    if ($self->{is_topping}) {
        die "盛り付け中だぞ!\n";
    }
    # 提供済みなのにまた茹でる?
    if ($self->{is_served}) {
        die "もう出しただろ!\n";
    }

    $self->{is_boiling} = 1;
    $self->{is_ready}   = 0;
    return "茹で開始: $self->{order_name}";
}

俺はモニターから目を逸らせなかった。改めて他人の目の前で見ると……いや、動いてるんだよ。動いてるんだけど——。

「32通り」

ドクターが短く言った。

俺は首をかしげた。ナナコさんが補足してくれる。

「フラグが5本でそれぞれON/OFFですから、組み合わせは2の5乗で32通りになります。でも有効な調理状態は6つだけですよね? 注文待ち、仕込み中、茹で中、盛り付け中、提供済み、エラー」

「26通りのハズレ……こりゃ闇鍋みたいなもんだな」

思わず口をついて出た。

診断

ドクターが立ち上がり、腕を組んだ。

「免疫不全」

「俺のコードが免疫不全だって? 筋トレと気合が足りねぇってことか?」

ナナコさんが首を横に振る。

「気合の問題ではないんです。フラグで状態を管理する設計そのものが、矛盾した状態を許してしまう構造なんです。調理に例えると……注文票に『仕込み中』『茹で中』『盛り付け中』のチェックボックスが並んでいて、全部に同時にチェックを入れられてしまう。そんな注文票で調理を管理しているようなものです」

俺は黙った。確かに、うちの店ではそんな注文票は使わなかった。工程カンバン——一枚のカードをフックに掛け替えるだけだ。「仕込み中」のフックにカードがあれば、それは仕込み中。茹でに進むなら、カードを「茹で中」のフックに移す。2つのフックに同時にカードが掛かることは 物理的にありえない

「フラグ免疫不全症候群」

ドクターが診断名を短く告げた。

「……大げさな名前つけやがって」

俺はそう言ったが、内心では認めざるを得なかった。

処方箋

ドクターが俺のPCのキーボードに手を伸ばした。

「おい、勝手に——」

「State パターン」

短い一言。ナナコさんが翻訳してくれる。

「チェックボックスの注文票をやめて、工程ごとに1枚のカードを作るんです。『仕込みカード』『茹でカード』『盛り付けカード』……カードには『この工程でできること』と『次にどのカードに進めるか』だけが書いてあります」

「カード制……まるで調理場の工程カンバンだな!」

俺はひざを叩いた。ラーメン屋時代に毎日使っていたあのカンバンが、コードの設計に化けるとは。

「まさにそのイメージです。カンバンが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
25
26
27
28
29
30
31
32
33
34
35
package MenMaster::State;
use v5.36;

sub new($class, %args) {
    return bless { context => $args{context} }, $class;
}

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

sub take_order($self, $, $ = undef) {
    die $self->_error('注文受付');
}

sub start_boiling($self) {
    die $self->_error('茹で開始');
}

sub start_topping($self) {
    die $self->_error('盛り付け');
}

sub serve($self) {
    die $self->_error('提供');
}

sub name($self) {
    my $class = ref $self;
    $class =~ s/.*:://;
    return $class;
}

sub _error($self, $action) {
    return sprintf "%s状態では「%s」はできません\n",
        $self->name, $action;
}

「おい、全部 die してるぞ。何もできないカードなんて意味あるのか?」

ナナコさんが笑った。「これは 雛形 です。『基本的にはどの操作もできない』というルールがまず最初にあって、各工程カードが自分に許可された操作だけを上書きする んです」

ドクターの指が止まらない。次に書かれたのは、各工程の「カード」だ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package MenMaster::State::WaitingOrder;
use v5.36;
use parent 'MenMaster::State';

sub take_order($self, $order_name, $noodle_type = 'regular') {
    my $ctx = $self->context;
    $ctx->{order_name}  = $order_name;
    $ctx->{noodle_type} = $noodle_type;
    $ctx->transition_to('MenMaster::State::Preparing');
    return "注文OK: $order_name ($noodle_type)";
}
1
2
3
4
5
6
7
8
9
package MenMaster::State::Boiling;
use v5.36;
use parent 'MenMaster::State';

sub start_topping($self) {
    my $ctx = $self->context;
    $ctx->transition_to('MenMaster::State::Topping');
    return "盛り付け開始: $ctx->{order_name}";
}

「……おい」

俺は画面を凝視した。

「フラグが……全部、消えてるぞ?」

「そうなんです」ナナコさんが頷く。「フラグはもう不要です。『今どの工程カードを持っているか』だけで状態が決まりますから」

俺は頭をガシガシ掻いた。

「WaitingOrder カードには take_order だけが書いてある。Boiling カードには start_topping だけ。つまり——茹で中の状態から できること は『盛り付けを始める』だけで、注文を受けたり提供したりは できない ……」

「ご名答です。そして、できない操作を呼び出そうとすると、先ほどの雛形が『この状態ではその操作はできません』とエラーを返してくれます」

ドクターが最後のピースを書き上げた。麺マスター本体のコードだ。

 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
package MenMaster;
use v5.36;

sub new($class) {
    my $self = bless {
        order_name  => '',
        noodle_type => 'regular',
        state       => undef,
    }, $class;
    $self->transition_to('MenMaster::State::WaitingOrder');
    return $self;
}

sub transition_to($self, $state_class) {
    $self->{state} = $state_class->new(context => $self);
}

sub take_order($self, $order_name, $noodle_type = 'regular') {
    $self->{state}->take_order($order_name, $noodle_type);
}

sub start_boiling($self) {
    $self->{state}->start_boiling;
}

sub start_topping($self) {
    $self->{state}->start_topping;
}

sub serve($self) {
    $self->{state}->serve;
}

俺は息を呑んだ。

「メインのコードが……こんだけかよ。全部、カードに委譲してるだけじゃねぇか」

委譲。

ドクターが短く頷いた。

「大将に任せるってことか!」

俺は思わず叫んだ。ドクターが一瞬、怪訝な顔をした気がしたが、すぐにモニターに視線を戻した。

その時、俺は急に思いついた。

「ちょっと待ってくれ」

俺は立ち上がって台所に向かった。

「おい、何をして——」

ナナコさんが驚いた顔をしている。

「今の話、実際にラーメン作りながら確認したい」

俺は寸胴鍋に火をかけながら、モニターを振り返った。

「ほら、今が WaitingOrder 状態だろ。注文が入った——味噌ラーメン一丁! ここで take_order が呼ばれて、Preparing に遷移する」

麺を取り出す。

「で、仕込みが終わったら start_boiling で Boiling 状態。麺を鍋に入れる——」

湯気が立ち上る。

「茹で上がったら start_topping で Topping 状態。チャーシュー、ネギ、メンマ——ここで start_boiling を呼んでも、Topping カードには start_boiling が書いてないからエラーになる。つまり 茹で直しは物理的にできない 。完璧だ!」

俺は盛り付けを終えて、どんぶりを3つ並べた。

「提供! serve で Completed 状態! ——お待ちどう」

ドクターの前にラーメンを置いた。ナナコさんの分も。ドクターは黙って箸を取り、一口すすった。

「……美味い」

俺は満面の笑みで——って、待て。

「コードの話じゃなかったのか!?」

「麺。硬さ、完璧」

ドクターは表情を変えずに、二口目をすすっている。いや、コードの話をしてくれ。

ナナコさんがくすりと笑った。「先生、ラーメンではなくコードの感想もお願いしますね」

ドクターは答えなかった。ただ、黙々とラーメンを食べ続けている。その背中に威厳があった。……いや、ただ食い意地が張ってるだけかもしれない。

ラーメンを作りながらStateパターンを実演する麺田と、黙々とラーメンを食べるドクター、笑顔で見守るナナコ

術後経過

全員がラーメンを前にしている状態で——ナナコさんの分も俺が作った——ドクターがテストの実行結果を見せた。

1
2
3
4
5
6
7
8
9
ok 1 - 初期状態は注文待ち
ok 2 - 注文受付
ok 3 - 仕込み状態に遷移
ok 4 - 茹で開始
ok 5 - 茹で中状態に遷移
ok 6 - 盛り付け開始
ok 7 - 盛り付け中状態に遷移
ok 8 - 提供
ok 9 - 完了状態に遷移

「全テスト通過」

ドクターが静かに言った。

ナナコさんが補足する。「すべてのテストが通りました。特に、以前バグが出ていた『無効な状態遷移のテスト』——茹で中から直接提供に飛ぶケースや、完了状態から茹でを始めるケース——がきちんとエラーになっています」

1
2
3
4
5
ok - 注文前に茹でるとエラー
ok - 茹で前に盛り付けるとエラー
ok - 仕込みから直接提供はエラー
ok - 茹で中から直接提供はエラー
ok - 完了から茹でるのはエラー

「マジかよ……あのバグ、3週間格闘してたんだぞ」

「新しい工程を追加したい場合も、新しい状態クラスを1つ作るだけです。既存のコードを変更する必要はありませんよ」

「トッピングのバリエーション増やしても大丈夫ってことか……味玉状態とか、背脂状態とか」

ナナコさんは微笑んだ。「ええ、そうです。工程カンバンにカードを1枚足すだけですからね」

ナナコさんがお茶を淹れて、まずドクターに渡した。ラーメンの後のお茶。自然な流れだ。

ドクターが受け取る。一瞬——ほんの一瞬だけ、指が触れたように見えた。

ドクターの顔がわずかに緩んだ。「……いい」と小さく呟いた。

お茶がそんなに美味いのか? いや……ナナコさんの手が——いやいや、考えすぎだ。

ナナコさんはもう俺のノートPCに戻って、追加のリファクタリングポイントを確認していた。完全にスルーだ。ドクターの肩が一瞬だけ落ちた気がしたが、すぐに姿勢を正した。……気のせいだろう。

退院指導

ドクターとナナコさんが帰り支度を始めた。

「今後、状態が増えても1クラス追加するだけです。他の状態に影響はありませんので、安心して拡張してくださいね」

ナナコさんが最後のアドバイスをくれる。俺は思わず頭を下げた。

「先生! 弟子にしてくれ!」

ドクターが少し驚いた顔をした。

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

「先生、弟子は取っていないんです。でも、いつでもご相談くださいね」

ナナコさんが柔らかく断る。

「そうか……じゃあせめて、店に来い! ラーメン、いくらでも奢るぜ! ……まぁ、もう店は閉めちまったんだけどな。出前ならいつでも!」

ドクターの背中が一瞬だけ止まった。少し嬉しそうに見えた——いや、あいつの感情は読めねぇ。黒い鞄を持ったまま、ドアへ向かう。

あの背中……大将って感じだな。……って、俺が元大将だけど。

ドアが閉まった後、俺は一人でPCに向き合った。State パターンで書き直された麺マスターのコードを眺める。フラグは1本もない。代わりに、6枚の工程カードがきれいに並んでいる。

俺はキーボードに手を置いた。次は何を作ろうか。味玉トッピングの状態クラス? いや、まずは基本の6工程で仕上げよう。

ラーメンと同じだ。基本がしっかりしてなきゃ、トッピングも映えない。


処方箋まとめ

症状適用すべき経過観察
booleanフラグが3本以上で状態を管理している
フラグの組み合わせで無効な状態が発生する
状態に応じて振る舞いが変わるオブジェクトがある
新しい状態を追加すると既存のコードに影響が出る
状態が2つだけで、今後増える予定がない
状態遷移のルールが単純で、条件分岐で十分管理できる

治療のステップ

  1. State(基底クラス)の定義: すべての操作をデフォルトでエラーにする雛形を作成する。
  2. ConcreteState(具体状態)の実装: 各状態ごとにサブクラスを作り、その状態で許可される操作だけをオーバーライドする。
  3. Context(コンテキスト)の実装: 現在の状態オブジェクトを保持し、操作を状態に委譲する transition_to メソッドで状態遷移を制御する。
  4. 遷移ルールの宣言: 各ConcreteStateの操作メソッド内で transition_to を呼び、次の状態への遷移先を明示する。
  5. 拡張: 新しい状態はConcreteStateクラスを1つ追加するのみ。既存の状態クラスやContextの修正は不要。

助手より

10年間厨房に立ち続けた経験からコードを書けるのは、麺田さんならではの強みです。Stateパターンは、まさに麺田さんが厨房で使っていた工程カンバンと同じ考え方なんです。「今どのカードがかかっているか」で全てが決まる——シンプルで、間違いようがない仕組みですよね。これからは後輩の方にも安心して麺マスターを使ってもらえるはずです。美味しいラーメン、ごちそうさまでした。

——ナナコ

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