@nqounetです。
前回は、状態ごとにクラスを作ることで、if/elseの肥大化問題を解決しました。
しかし、まだ問題が残っています。すべての状態クラスが同じメソッド(insert_coin、select_product、dispense)を持つ必要がありますが、それを強制する仕組みがありません。
今回は、Moo::Roleを使って「共通の約束」を定義しましょう。
問題点を確認する
前回作った状態クラスを振り返ってみましょう。
WaitingStateもCoinInsertedStateも、同じ3つのメソッドを持っています。
insert_coinselect_productdispense
しかし、もし新しい状態クラスを作るときに、うっかりメソッドを1つ忘れてしまったらどうなるでしょうか?
実行時にエラーが発生してしまいます。これを事前に防ぐ仕組みが欲しいですね。
Moo::Roleでインターフェースを定義する
Moo::Roleのrequiresを使うと、「このロールを使うクラスは、これらのメソッドを必ず実装してください」という約束を定義できます。
1
2
3
4
5
6
7
| package VendingMachineState {
use Moo::Role;
requires 'insert_coin';
requires 'select_product';
requires 'dispense';
}
|
このロールは「自動販売機の状態として振る舞うなら、3つのメソッドを必ず持ってください」という約束です。
状態クラスにロールを適用する
各状態クラスでwithを使ってロールを適用します。
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
| #!/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 WaitingState->new;
}
sub dispense ($self) {
say "先に商品を選択してください";
return $self;
}
}
# 動作確認
say "=== VendingMachineStateロールのテスト ===";
say "";
my $state = WaitingState->new;
# ロールが適用されているか確認
if ($state->does('VendingMachineState')) {
say "WaitingStateはVendingMachineStateロールを持っています";
}
say "";
say "[操作] コインを投入";
$state = $state->insert_coin;
say "現在の状態: " . ref($state);
if ($state->does('VendingMachineState')) {
say "CoinInsertedStateもVendingMachineStateロールを持っています";
}
say "";
say "[操作] 商品を選択";
$state = $state->select_product;
say "現在の状態: " . ref($state);
|
requiresの効果を確認する
もし、メソッドを実装し忘れたらどうなるでしょうか?
試しに、dispenseメソッドを削除してみましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| package BrokenState {
use Moo;
use v5.36;
with 'VendingMachineState';
sub insert_coin ($self) {
say "コインを受け付けました";
return $self;
}
sub select_product ($self) {
say "商品を選択しました";
return $self;
}
# dispenseメソッドがない!
}
|
このコードを実行すると、以下のようなエラーが発生します。
1
| Can't apply VendingMachineState to BrokenState - missing dispense
|
コンパイル時(正確にはwithの時点)でエラーになるため、うっかりミスを早期に発見できます。
今回のポイント
Moo::Roleのrequiresを使うことで、以下のメリットが得られます。
- すべての状態クラスが統一されたAPIを持つことを保証できる
- メソッドの実装漏れを早期に発見できる
- 新しい状態クラスを作るときの「チェックリスト」になる
今回の完成コード
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
| #!/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 WaitingState->new;
}
sub dispense ($self) {
say "先に商品を選択してください";
return $self;
}
}
# 動作確認
say "=== VendingMachineStateロールのテスト ===";
say "";
my $state = WaitingState->new;
# ロールが適用されているか確認
if ($state->does('VendingMachineState')) {
say "WaitingStateはVendingMachineStateロールを持っています";
}
say "";
say "[操作] コインを投入";
$state = $state->insert_coin;
say "現在の状態: " . ref($state);
if ($state->does('VendingMachineState')) {
say "CoinInsertedStateもVendingMachineStateロールを持っています";
}
say "";
say "[操作] 商品を選択";
$state = $state->select_product;
say "現在の状態: " . ref($state);
|
まとめ
VendingMachineStateロールを作成し、必須メソッドをrequiresで定義しました- 各状態クラスで
with 'VendingMachineState'を宣言しました - メソッドの実装漏れがあれば、
withの時点でエラーになります $object->does('RoleName')でロールの適用を確認できます
次回「第5回-状態を管理するクラスを作ろう」では、状態を一元管理するContextクラスを作成します。お楽しみに!