@nqounetです。
前回は、if/elseを使って「待機中」と「コイン投入済み」の2つの状態を管理する自動販売機を作りました。
今回は、より現実的な自動販売機を目指して、状態を追加してみましょう。
状態を増やしたい
実際の自動販売機には、もう少し多くの状態があります。
- 待機中(waiting): コインが投入されるのを待っている
- コイン投入済み(coin_inserted): コインが入っていて、商品を選べる状態
- 払い出し中(dispensing): 商品を払い出している最中
- 売り切れ(sold_out): 在庫がなくなった状態
これらの状態を追加すると、コードはどうなるでしょうか?
if/elseを増やしてみる
前回のコードに「払い出し中」と「売り切れ」の状態を追加してみます。
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
| #!/usr/bin/env perl
use v5.36;
use Moo;
package VendingMachine {
use Moo;
has state => (
is => 'rw',
default => sub { 'waiting' },
);
has stock => (
is => 'rw',
default => sub { 5 },
);
sub insert_coin ($self) {
if ($self->state eq 'waiting') {
say "コインを受け付けました";
$self->state('coin_inserted');
}
elsif ($self->state eq 'coin_inserted') {
say "すでにコインが入っています";
}
elsif ($self->state eq 'dispensing') {
say "払い出し中です。お待ちください";
}
elsif ($self->state eq 'sold_out') {
say "売り切れです。コインを受け付けられません";
}
}
sub select_product ($self) {
if ($self->state eq 'waiting') {
say "先にコインを入れてください";
}
elsif ($self->state eq 'coin_inserted') {
if ($self->stock > 0) {
say "商品を選択しました。払い出しを開始します";
$self->state('dispensing');
}
else {
say "申し訳ありません。売り切れです";
say "コインを返却します";
$self->state('sold_out');
}
}
elsif ($self->state eq 'dispensing') {
say "払い出し中です。お待ちください";
}
elsif ($self->state eq 'sold_out') {
say "売り切れです";
}
}
sub dispense ($self) {
if ($self->state eq 'waiting') {
say "払い出す商品がありません";
}
elsif ($self->state eq 'coin_inserted') {
say "先に商品を選択してください";
}
elsif ($self->state eq 'dispensing') {
say "商品を払い出しました";
$self->stock($self->stock - 1);
say "残り在庫: " . $self->stock . "個";
if ($self->stock > 0) {
$self->state('waiting');
}
else {
say "在庫がなくなりました";
$self->state('sold_out');
}
}
elsif ($self->state eq 'sold_out') {
say "売り切れです。払い出せません";
}
}
}
# 動作確認
my $vm = VendingMachine->new(stock => 2);
say "=== 自動販売機シミュレーター(在庫2個)===";
say "";
# 1回目の購入
say "[操作] コインを投入";
$vm->insert_coin;
say "[操作] 商品を選択";
$vm->select_product;
say "[操作] 払い出し";
$vm->dispense;
say "";
# 2回目の購入
say "[操作] コインを投入";
$vm->insert_coin;
say "[操作] 商品を選択";
$vm->select_product;
say "[操作] 払い出し";
$vm->dispense;
say "";
# 売り切れ後
say "[操作] コインを投入";
$vm->insert_coin;
|
問題点を整理しよう
このコードを見て、何か気づくことはありませんか?
問題点を整理してみましょう。
- 各メソッドに同じ状態チェックのパターンが繰り返されている
- 状態が4つあるので、各メソッドに4つの分岐がある
- 新しい状態を追加するたびに、すべてのメソッドを修正する必要がある
- どの状態でどんな処理をするのか、全体像が把握しにくい
- 状態の追加を忘れると、バグが発生する可能性がある
これはコードスメル(code smell)と呼ばれる、良くない兆候です。
状態が増えるとさらに大変
もし「メンテナンス中」「返金中」といった状態をさらに追加したらどうなるでしょうか?
- 各メソッドのif/elseがさらに長くなる
- コードの見通しがますます悪くなる
- テストも複雑になる
- バグが入り込みやすくなる
この問題をどう解決すればよいでしょうか?
次回、この問題を解決するための第一歩として、「状態ごとにクラスを作る」アプローチを試してみましょう。
今回の完成コード
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
| #!/usr/bin/env perl
use v5.36;
use Moo;
package VendingMachine {
use Moo;
has state => (
is => 'rw',
default => sub { 'waiting' },
);
has stock => (
is => 'rw',
default => sub { 5 },
);
sub insert_coin ($self) {
if ($self->state eq 'waiting') {
say "コインを受け付けました";
$self->state('coin_inserted');
}
elsif ($self->state eq 'coin_inserted') {
say "すでにコインが入っています";
}
elsif ($self->state eq 'dispensing') {
say "払い出し中です。お待ちください";
}
elsif ($self->state eq 'sold_out') {
say "売り切れです。コインを受け付けられません";
}
}
sub select_product ($self) {
if ($self->state eq 'waiting') {
say "先にコインを入れてください";
}
elsif ($self->state eq 'coin_inserted') {
if ($self->stock > 0) {
say "商品を選択しました。払い出しを開始します";
$self->state('dispensing');
}
else {
say "申し訳ありません。売り切れです";
say "コインを返却します";
$self->state('sold_out');
}
}
elsif ($self->state eq 'dispensing') {
say "払い出し中です。お待ちください";
}
elsif ($self->state eq 'sold_out') {
say "売り切れです";
}
}
sub dispense ($self) {
if ($self->state eq 'waiting') {
say "払い出す商品がありません";
}
elsif ($self->state eq 'coin_inserted') {
say "先に商品を選択してください";
}
elsif ($self->state eq 'dispensing') {
say "商品を払い出しました";
$self->stock($self->stock - 1);
say "残り在庫: " . $self->stock . "個";
if ($self->stock > 0) {
$self->state('waiting');
}
else {
say "在庫がなくなりました";
$self->state('sold_out');
}
}
elsif ($self->state eq 'sold_out') {
say "売り切れです。払い出せません";
}
}
}
# 動作確認
my $vm = VendingMachine->new(stock => 2);
say "=== 自動販売機シミュレーター(在庫2個)===";
say "";
# 1回目の購入
say "[操作] コインを投入";
$vm->insert_coin;
say "[操作] 商品を選択";
$vm->select_product;
say "[操作] 払い出し";
$vm->dispense;
say "";
# 2回目の購入
say "[操作] コインを投入";
$vm->insert_coin;
say "[操作] 商品を選択";
$vm->select_product;
say "[操作] 払い出し";
$vm->dispense;
say "";
# 売り切れ後
say "[操作] コインを投入";
$vm->insert_coin;
|
まとめ
- 「払い出し中」「売り切れ」の状態を追加したら、if/elseが肥大化しました
- 状態が増えるほど、各メソッドの条件分岐が複雑になります
- 新しい状態を追加するたびに、すべてのメソッドを修正する必要があります
- これは「コードスメル」と呼ばれる、リファクタリングが必要なサインです
次回「第3回-状態ごとにクラスを作ろう」では、この問題を解決するために状態をクラスとして分離します。お楽しみに!