Featured image of post 第5回-状態を管理するContextクラスを作ろう - Mooを使って自動販売機シミュレーターを作ってみよう

第5回-状態を管理するContextクラスを作ろう - Mooを使って自動販売機シミュレーターを作ってみよう

状態を一元管理するContextクラスを作成。VendingMachineクラスがStateへ処理を委譲する仕組みをMooで実装します。

@nqounetです。

前回は、Moo::Roleを使って状態クラスに共通のインターフェースを定義しました。

今回は、状態を一元管理する「Context」クラスを作成しましょう。これにより、利用者は状態クラスを直接操作することなく、自動販売機を使えるようになります。

なぜContextクラスが必要なのか

前回までのコードでは、利用者が直接状態オブジェクトを操作していました。

1
2
3
my $state = WaitingState->new;
$state = $state->insert_coin;
$state = $state->select_product;

これには以下の問題があります。

  • 利用者が状態オブジェクトを管理する必要がある
  • 状態クラスの存在を利用者が知っている必要がある
  • 在庫などの追加情報を管理する場所がない

Contextクラスを作ることで、これらの問題を解決できます。

VendingMachineクラス(Context)を作る

自動販売機本体を表す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
package VendingMachine {
    use Moo;
    use v5.36;

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

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

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

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

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

    sub current_state_name ($self) {
        return ref($self->state);
    }
}

Contextクラスのポイント

VendingMachineクラスの特徴を整理しましょう。

  • state属性で現在の状態オブジェクトを保持する
  • stock属性で在庫を管理する(今後使用)
  • 各操作メソッドは、状態オブジェクトに処理を委譲する
  • 戻り値の新しい状態オブジェクトをstateに設定する

利用者はVendingMachineクラスだけを使えばよく、内部の状態クラスを意識する必要がありません。

完成コードと動作確認

状態クラス、ロール、Contextクラスをすべて組み合わせた完成コードです。

  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
#!/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) {
        say "コインを受け付けました";
        return CoinInsertedState->new;
    }

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

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

package CoinInsertedState {
    use Moo;
    use v5.36;

    with 'VendingMachineState';

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

    sub select_product ($self) {
        say "商品を選択しました。払い出しを開始します";
        return DispensingState->new;
    }

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

package DispensingState {
    use Moo;
    use v5.36;

    with 'VendingMachineState';

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

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

    sub dispense ($self) {
        say "商品を払い出しました";
        return 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 insert_coin ($self) {
        my $next_state = $self->state->insert_coin;
        $self->state($next_state);
    }

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

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

    sub current_state_name ($self) {
        return ref($self->state);
    }
}

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

my $vm = VendingMachine->new;

say "初期状態: " . $vm->current_state_name;
say "";

say "[操作] 商品を選択(コインなし)";
$vm->select_product;
say "現在の状態: " . $vm->current_state_name;
say "";

say "[操作] コインを投入";
$vm->insert_coin;
say "現在の状態: " . $vm->current_state_name;
say "";

say "[操作] 商品を選択";
$vm->select_product;
say "現在の状態: " . $vm->current_state_name;
say "";

say "[操作] 払い出し";
$vm->dispense;
say "現在の状態: " . $vm->current_state_name;

実行結果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
=== 自動販売機シミュレーター ===

初期状態: WaitingState

[操作] 商品を選択(コインなし)
先にコインを入れてください
現在の状態: WaitingState

[操作] コインを投入
コインを受け付けました
現在の状態: CoinInsertedState

[操作] 商品を選択
商品を選択しました。払い出しを開始します
現在の状態: DispensingState

[操作] 払い出し
商品を払い出しました
現在の状態: WaitingState

今回のポイント

Contextクラス(VendingMachine)を作ることで、以下のメリットが得られました。

  • 利用者はVendingMachineクラスだけを使えばよい
  • 内部の状態クラスは隠蔽されている
  • 在庫などの追加情報を管理する場所ができた
  • 各操作は状態オブジェクトに委譲される

しかし、まだ問題があります。状態クラスの中でVendingMachine(Context)の情報(例えば在庫)にアクセスできません。

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

まとめ

  • VendingMachineクラス(Context)を作成し、状態を一元管理しました
  • 各操作メソッドは状態オブジェクトに処理を委譲しています
  • 戻り値の新しい状態をstate属性に設定することで状態遷移します
  • 利用者は内部の状態クラスを意識する必要がありません

次回「第6回-状態の中から次の状態へ遷移しよう」では、StateがContextへの参照を受け取る仕組みを実装します。お楽しみに!

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