Featured image of post 第6回-状態の中から次の状態へ遷移しよう - Mooを使って自動販売機シミュレーターを作ってみよう

第6回-状態の中から次の状態へ遷移しよう - Mooを使って自動販売機シミュレーターを作ってみよう

状態自身が次の状態へ遷移する仕組みを実装!StateがContextへの参照を受け取り、自ら状態を切り替える設計を学びます。

@nqounetです。

前回は、VendingMachineクラス(Context)を作成して、状態を一元管理できるようになりました。

しかし、状態クラスの中からVendingMachineの情報(在庫など)にアクセスできないという問題がありました。

今回は、状態クラスがContextへの参照を受け取り、自ら状態遷移を行う仕組みを実装します。

現在の問題点

前回のコードでは、状態クラスは次の状態オブジェクトを返すだけでした。

1
2
3
4
sub select_product ($self) {
    say "商品を選択しました";
    return DispensingState->new;
}

この設計では、以下のことができません。

  • VendingMachineの在庫を確認する
  • VendingMachineの在庫を減らす
  • 在庫に応じて遷移先を変える

解決策:Contextを引数で渡す

状態クラスのメソッドにContext(VendingMachine)への参照を渡すようにします。

これにより、状態クラスの中から以下のことができるようになります。

  • Contextの情報(在庫など)を参照する
  • Contextの状態を直接変更する
  • Contextの情報に基づいて遷移先を決める

新しい状態クラスの実装

状態クラスのメソッドにContextを引数として追加します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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 "払い出す商品がありません";
    }
}

ポイントは以下の通りです。

  • 各メソッドの第2引数に$contextを追加
  • 状態遷移は$context->set_state()を呼び出す
  • 状態が変わらない場合は何もしない(戻り値は不要)

Contextクラスの変更

VendingMachineクラスにset_stateメソッドを追加し、状態クラスに自分自身を渡すようにします。

 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
package VendingMachine {
    use Moo;
    use v5.36;

    has state => (
        is      => 'rw',
        default => sub { WaitingState->new },
    );

    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);
    }
}

変更点は以下の通りです。

  • set_stateメソッドを追加
  • 各操作メソッドで、状態クラスに$self(VendingMachine自身)を渡す
  • 戻り値を受け取って状態を設定する処理は不要になった

完成コードと動作確認

在庫管理も含めた完成コードです。

  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
#!/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(WaitingState->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 . "個";
        $context->set_state(WaitingState->new);
    }
}

package VendingMachine {
    use Moo;
    use v5.36;

    has state => (
        is      => 'rw',
        default => sub { WaitingState->new },
    );

    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) {
        return ref($self->state);
    }
}

# 動作確認
say "=== 自動販売機シミュレーター ===";
say "";

my $vm = VendingMachine->new(stock => 2);

say "初期在庫: " . $vm->stock . "個";
say "";

for my $i (1..3) {
    say "--- 購入 $i 回目 ---";
    say "[操作] コインを投入";
    $vm->insert_coin;
    say "[操作] 商品を選択";
    $vm->select_product;
    say "[操作] 払い出し";
    $vm->dispense;
    say "";
}

実行結果

 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
=== 自動販売機シミュレーター ===

初期在庫: 2個

--- 購入 1 回目 ---
[操作] コインを投入
コインを受け付けました
[操作] 商品を選択
商品を選択しました。払い出しを開始します
[操作] 払い出し
商品を払い出しました
残り在庫: 1個

--- 購入 2 回目 ---
[操作] コインを投入
コインを受け付けました
[操作] 商品を選択
商品を選択しました。払い出しを開始します
[操作] 払い出し
商品を払い出しました
残り在庫: 0個

--- 購入 3 回目 ---
[操作] コインを投入
コインを受け付けました
[操作] 商品を選択
申し訳ありません。売り切れです
コインを返却します
[操作] 払い出し
払い出す商品がありません

在庫が0になったとき、商品選択で「売り切れ」と表示されるようになりました。

今回のポイント

状態クラスにContextへの参照を渡すことで、以下のことができるようになりました。

  • 状態クラスからContextの情報(在庫)を参照できる
  • 状態クラスからContextの状態を直接変更できる
  • Contextの情報に基づいて遷移先を動的に決められる

これが状態クラスの本来の力です。状態自身が「次にどの状態になるか」を決定できます。

まとめ

  • 状態クラスのメソッドにContext(VendingMachine)を引数として渡すようにしました
  • 状態クラスから$context->set_state()で状態遷移を行います
  • 状態クラスから$context->stockで在庫を参照・変更できます
  • 在庫に応じて遷移先を動的に決められるようになりました

次回「第7回-型チェックでバグを防ごう」では、does制約を使って型チェックを追加します。お楽しみに!

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