@nqounetです。
前回は、状態を増やしたらif/elseが肥大化してしまう問題を見ました。
今回は、この問題を解決するために「状態ごとにクラスを作る」アプローチを試してみましょう。
状態をクラスにするアイデア
前回のコードでは、1つのクラスの中で「待機中のときはこうする」「コイン投入済みのときはこうする」という分岐を書いていました。
発想を変えて、「待機中」という状態そのものをクラスにしてみましょう。
- WaitingState: 待機中の状態を表すクラス
- CoinInsertedState: コイン投入済みの状態を表すクラス
それぞれのクラスが、その状態での振る舞いを知っているようにします。
WaitingStateクラスを作る
まず、「待機中」の状態を表すクラスを作ります。
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;
sub insert_coin ($self) {
say "コインを受け付けました";
return CoinInsertedState->new;
}
sub select_product ($self) {
say "先にコインを入れてください";
return $self;
}
sub dispense ($self) {
say "払い出す商品がありません";
return $self;
}
}
|
このクラスのポイントは以下の通りです。
insert_coinは次の状態(CoinInsertedState)を返すselect_productとdispenseは自分自身を返す(状態が変わらない)- if/elseがない!このクラスは「待機中」のことだけを知っている
CoinInsertedStateクラスを作る
次に、「コイン投入済み」の状態を表すクラスを作ります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| package CoinInsertedState {
use Moo;
use v5.36;
sub insert_coin ($self) {
say "すでにコインが入っています";
return $self;
}
sub select_product ($self) {
say "商品を選択しました";
return DispensingState->new;
}
sub dispense ($self) {
say "先に商品を選択してください";
return $self;
}
}
|
このクラスも同様に、「コイン投入済み」のときの振る舞いだけを定義しています。
動作確認コード
2つの状態クラスを使って、簡単な動作確認をしてみましょう。
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
| #!/usr/bin/env perl
use v5.36;
use Moo;
package WaitingState {
use Moo;
use v5.36;
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;
sub insert_coin ($self) {
say "すでにコインが入っています";
return $self;
}
sub select_product ($self) {
say "商品を選択しました";
# 簡略化のため、払い出し後は待機状態に戻る
return WaitingState->new;
}
sub dispense ($self) {
say "先に商品を選択してください";
return $self;
}
}
# 動作確認
say "=== 状態クラスのテスト ===";
say "";
my $state = WaitingState->new;
say "[操作] 商品を選択(待機中)";
$state = $state->select_product;
say "現在の状態: " . ref($state);
say "";
say "[操作] コインを投入";
$state = $state->insert_coin;
say "現在の状態: " . ref($state);
say "";
say "[操作] 商品を選択";
$state = $state->select_product;
say "現在の状態: " . ref($state);
|
実行結果
1
2
3
4
5
6
7
8
9
10
11
12
13
| === 状態クラスのテスト ===
[操作] 商品を選択(待機中)
先にコインを入れてください
現在の状態: WaitingState
[操作] コインを投入
コインを受け付けました
現在の状態: CoinInsertedState
[操作] 商品を選択
商品を選択しました
現在の状態: WaitingState
|
何が改善されたか
状態をクラスに分離したことで、以下の改善がありました。
- 各状態クラスは自分のことだけを知っている(単一責任の原則)
- if/elseがなくなり、コードの見通しが良くなった
- 新しい状態を追加しても、既存のクラスを変更する必要が少ない
しかし、まだ問題があります。
- すべての状態クラスが同じメソッドを持つ必要があるが、それを強制する仕組みがない
- 状態クラスを使う側のコードがまだ整理されていない
次回は、Moo::Roleを使って「すべての状態クラスが持つべきメソッド」を定義します。
今回の完成コード
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
| #!/usr/bin/env perl
use v5.36;
use Moo;
package WaitingState {
use Moo;
use v5.36;
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;
sub insert_coin ($self) {
say "すでにコインが入っています";
return $self;
}
sub select_product ($self) {
say "商品を選択しました";
# 簡略化のため、払い出し後は待機状態に戻る
return WaitingState->new;
}
sub dispense ($self) {
say "先に商品を選択してください";
return $self;
}
}
# 動作確認
say "=== 状態クラスのテスト ===";
say "";
my $state = WaitingState->new;
say "[操作] 商品を選択(待機中)";
$state = $state->select_product;
say "現在の状態: " . ref($state);
say "";
say "[操作] コインを投入";
$state = $state->insert_coin;
say "現在の状態: " . ref($state);
say "";
say "[操作] 商品を選択";
$state = $state->select_product;
say "現在の状態: " . ref($state);
|
まとめ
- 「待機中」と「コイン投入済み」をそれぞれ独立したクラスにしました
- 各クラスは自分の状態での振る舞いだけを知っています
- メソッドは次の状態を返すことで、状態遷移を表現しています
- if/elseがなくなり、コードの見通しが良くなりました
次回「第4回-共通の約束を決めよう」では、Moo::Roleを使ってすべての状態クラスに共通のインターフェースを定義します。お楽しみに!