@nqounetです。
前回は、isaを使って型チェックを追加しました。
今回は、新しい「売り切れ状態(SoldOutState)」を追加します。そして、既存のクラスをほとんど変更せずに機能拡張できることを確認しましょう。
OCP(開放閉鎖原則)とは
OCP(Open-Closed Principle、開放閉鎖原則)は、SOLID原則の1つです。
- 拡張に対して開いている: 新しい機能を追加できる
- 修正に対して閉じている: 既存のコードを変更しなくてよい
つまり、「新しいコードを追加するだけで機能拡張できる」という設計を目指します。
今回のケースで言えば、「SoldOutStateクラスを追加するだけで、売り切れ状態を実現できる」ということです。
SoldOutStateクラスを作る
売り切れ状態を表すSoldOutStateクラスを作成します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| 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 "売り切れです。払い出せません";
}
}
|
このクラスの特徴は以下の通りです。
VendingMachineStateロールを持つ- すべての操作で「売り切れ」と表示
- 状態遷移は行わない
既存クラスの変更点
新しい状態を追加したとき、既存のクラスで変更が必要な箇所を確認しましょう。
VendingMachineクラス(Context)は変更不要です!
変更が必要なのは、SoldOutStateへ遷移する箇所だけです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| package DispensingState {
# ...
sub dispense ($self, $context) {
say "商品を払い出しました";
$context->stock($context->stock - 1);
say "残り在庫: " . $context->stock . "個";
# 在庫が0になったらSoldOutStateへ遷移
if ($context->stock > 0) {
$context->set_state(WaitingState->new);
}
else {
say "在庫がなくなりました";
$context->set_state(SoldOutState->new);
}
}
}
|
DispensingStateのdispenseメソッドを修正して、在庫が0になったときにSoldOutStateへ遷移するようにします。
OCPの威力
今回の変更を振り返ってみましょう。
- SoldOutStateクラスを新規追加
- DispensingStateを1箇所だけ修正(遷移先の追加)
- VendingMachineState(ロール)は変更なし
- VendingMachine(Context)は変更なし
- WaitingStateは変更なし
- CoinInsertedStateは変更なし
ほとんどのコードは変更せずに、新しい状態を追加できました。これがOCPの威力です。
完成コード
SoldOutStateを追加した完成コードです。
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
| #!/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) {
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 "現在の状態: " . $vm->current_state_name;
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
31
32
33
| === 自動販売機シミュレーター(売り切れ対応版)===
初期在庫: 2個
--- 購入 1 回目 ---
[操作] コインを投入
コインを受け付けました
[操作] 商品を選択
商品を選択しました。払い出しを開始します
[操作] 払い出し
商品を払い出しました
残り在庫: 1個
現在の状態: WaitingState
--- 購入 2 回目 ---
[操作] コインを投入
コインを受け付けました
[操作] 商品を選択
商品を選択しました。払い出しを開始します
[操作] 払い出し
商品を払い出しました
残り在庫: 0個
在庫がなくなりました
現在の状態: SoldOutState
--- 購入 3 回目 ---
[操作] コインを投入
売り切れです。コインを受け付けられません
[操作] 商品を選択
売り切れです
[操作] 払い出し
売り切れです。払い出せません
現在の状態: SoldOutState
|
在庫がなくなると、SoldOutStateに遷移し、すべての操作で「売り切れ」と表示されます。
まとめ
- SoldOutStateクラスを新規追加しました
- 既存のVendingMachine(Context)は変更不要でした
- DispensingStateの遷移先を1箇所追加しただけです
- これがOCP(開放閉鎖原則)の威力です
- 新しいコードを追加するだけで機能拡張できます
次回「第9回-完成!自動販売機シミュレーター」では、全機能を統合して対話的なCLIで動作確認します。お楽しみに!