Featured image of post 第9回-完成!自動販売機シミュレーター - Mooを使って自動販売機シミュレーターを作ってみよう

第9回-完成!自動販売機シミュレーター - Mooを使って自動販売機シミュレーターを作ってみよう

いよいよ完成!全機能を統合した自動販売機シミュレーター。対話的なCLIで動作確認しながら、これまでの学習を振り返ります。

@nqounetです。

前回は、SoldOutState(売り切れ状態)を追加して、OCP(開放閉鎖原則)の威力を体感しました。

今回は、これまで作ってきた自動販売機シミュレーターを統合し、対話的なCLIで動作確認できるようにします。

これまでの振り返り

第1回から第8回までで、以下のことを学びました。

  • 第1回:if/elseで状態を管理する素朴な実装
  • 第2回:状態が増えるとif/elseが肥大化する問題
  • 第3回:状態ごとにクラスを分離
  • 第4回:Moo::Roleで共通インターフェースを定義
  • 第5回:VendingMachine(Context)クラスで状態を一元管理
  • 第6回:StateがContextへの参照を受け取り、自ら状態遷移
  • 第7回:does制約で型チェック
  • 第8回:SoldOutStateを追加してOCPを実践

これらをすべて統合した完成版を作りましょう。

対話的CLIの実装

ユーザーがコマンドを入力して操作できるCLIを追加します。

 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
sub run_cli ($vm) {
    say "=== 自動販売機シミュレーター ===";
    say "コマンド: c(コイン投入), s(商品選択), d(払い出し), q(終了)";
    say "";

    while (1) {
        print "現在の状態: " . $vm->current_state_name;
        print " / 在庫: " . $vm->stock . "個";
        print "\n> ";

        my $input = <STDIN>;
        last unless defined $input;
        chomp $input;

        if ($input eq 'c') {
            $vm->insert_coin;
        }
        elsif ($input eq 's') {
            $vm->select_product;
        }
        elsif ($input eq 'd') {
            $vm->dispense;
        }
        elsif ($input eq 'q') {
            say "終了します";
            last;
        }
        else {
            say "不明なコマンドです: $input";
        }
        say "";
    }
}

完成コード

対話的CLIを含む完成コードです。

  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
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
#!/usr/bin/env perl
use v5.36;
use Moo;

package VendingMachineState {
    use Moo::Role;

    requires 'insert_coin';
    requires 'select_product';
    requires 'dispense';
}

package WaitingState {
    use Moo;
    use v5.36;

    with 'VendingMachineState';

    sub insert_coin ($self, $context) {
        say "コインを受け付けました";
        $context->set_state(CoinInsertedState->new);
    }

    sub select_product ($self, $context) {
        say "先にコインを入れてください";
    }

    sub dispense ($self, $context) {
        say "払い出す商品がありません";
    }
}

package CoinInsertedState {
    use Moo;
    use v5.36;

    with 'VendingMachineState';

    sub insert_coin ($self, $context) {
        say "すでにコインが入っています";
    }

    sub select_product ($self, $context) {
        if ($context->stock > 0) {
            say "商品を選択しました。払い出しを開始します";
            $context->set_state(DispensingState->new);
        }
        else {
            say "申し訳ありません。売り切れです";
            say "コインを返却します";
            $context->set_state(SoldOutState->new);
        }
    }

    sub dispense ($self, $context) {
        say "先に商品を選択してください";
    }
}

package DispensingState {
    use Moo;
    use v5.36;

    with 'VendingMachineState';

    sub insert_coin ($self, $context) {
        say "払い出し中です。お待ちください";
    }

    sub select_product ($self, $context) {
        say "払い出し中です。お待ちください";
    }

    sub dispense ($self, $context) {
        say "商品を払い出しました";
        $context->stock($context->stock - 1);
        say "残り在庫: " . $context->stock . "個";

        if ($context->stock > 0) {
            $context->set_state(WaitingState->new);
        }
        else {
            say "在庫がなくなりました";
            $context->set_state(SoldOutState->new);
        }
    }
}

package SoldOutState {
    use Moo;
    use v5.36;

    with 'VendingMachineState';

    sub insert_coin ($self, $context) {
        say "売り切れです。コインを受け付けられません";
    }

    sub select_product ($self, $context) {
        say "売り切れです";
    }

    sub dispense ($self, $context) {
        say "売り切れです。払い出せません";
    }
}

package VendingMachine {
    use Moo;
    use v5.36;

    has state => (
        is      => 'rw',
        default => sub { WaitingState->new },
        isa    => sub { # 型チェック追加
            my $value = shift;
            die "state must do VendingMachineState role"
                unless $value->does('VendingMachineState');
        },
    );

    has stock => (
        is      => 'rw',
        default => sub { 5 },
    );

    sub set_state ($self, $new_state) {
        $self->state($new_state);
    }

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

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

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

    sub current_state_name ($self) {
        my $name = ref($self->state);
        my %ja_names = (
            WaitingState      => '待機中',
            CoinInsertedState => 'コイン投入済み',
            DispensingState   => '払い出し中',
            SoldOutState      => '売り切れ',
        );
        return $ja_names{$name} // $name;
    }
}

# 対話的CLI
sub run_cli ($vm) {
    say "=== 自動販売機シミュレーター ===";
    say "コマンド: c(コイン投入), s(商品選択), d(払い出し), q(終了)";
    say "";

    while (1) {
        print "現在の状態: " . $vm->current_state_name;
        print " / 在庫: " . $vm->stock . "個";
        print "\n> ";

        my $input = <STDIN>;
        last unless defined $input;
        chomp $input;

        if ($input eq 'c') {
            $vm->insert_coin;
        }
        elsif ($input eq 's') {
            $vm->select_product;
        }
        elsif ($input eq 'd') {
            $vm->dispense;
        }
        elsif ($input eq 'q') {
            say "終了します";
            last;
        }
        elsif ($input eq '') {
            next;
        }
        else {
            say "不明なコマンドです: $input";
        }
        say "";
    }
}

# メイン
my $vm = VendingMachine->new(stock => 3);
run_cli($vm);

使い方

スクリプトを実行すると、対話的なCLIが起動します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
$ perl vending_machine.pl
=== 自動販売機シミュレーター ===
コマンド: c(コイン投入), s(商品選択), d(払い出し), q(終了)

現在の状態: 待機中 / 在庫: 3個
> c
コインを受け付けました

現在の状態: コイン投入済み / 在庫: 3個
> s
商品を選択しました。払い出しを開始します

現在の状態: 払い出し中 / 在庫: 3個
> d
商品を払い出しました
残り在庫: 2個

現在の状態: 待機中 / 在庫: 2個
> q
終了します

状態遷移図

完成した自動販売機の状態遷移を図にすると、以下のようになります。

	stateDiagram-v2
    [*] --> WaitingState : 初期状態

    WaitingState --> CoinInsertedState : insert_coin

    CoinInsertedState --> DispensingState : select_product (在庫あり)
    CoinInsertedState --> SoldOutState : select_product (在庫なし)

    DispensingState --> WaitingState : dispense (在庫あり)
    DispensingState --> SoldOutState : dispense (在庫なし)

    SoldOutState --> [*] : (終了状態)

設計のまとめ

完成した自動販売機シミュレーターの設計を振り返りましょう。

登場するクラス・ロール:

名前役割
VendingMachineState状態クラスの共通インターフェース(ロール)
WaitingState待機中状態
CoinInsertedStateコイン投入済み状態
DispensingState払い出し中状態
SoldOutState売り切れ状態
VendingMachine状態を管理するContext

設計の特徴:

  • 状態ごとにクラスが分離されている
  • 各状態クラスは自分の振る舞いだけを知っている
  • VendingMachine(Context)は状態を保持し、操作を委譲する
  • 状態クラスがContextを受け取り、自ら状態遷移する
  • does制約で型安全性を確保

まとめ

  • 全機能を統合した自動販売機シミュレーターが完成しました
  • 対話的なCLIで実際に操作できます
  • 状態遷移が正しく動作することを確認しました
  • 状態ごとにクラスが分離された、保守しやすい設計になっています

次回「第10回-これがStateパターンだ!」では、私たちが作ってきた設計がデザインパターンの1つであることを明かします。お楽しみに!

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