Featured image of post 第4回-Moo::Roleで共通の約束を決めよう - Mooを使って自動販売機シミュレーターを作ってみよう

第4回-Moo::Roleで共通の約束を決めよう - Mooを使って自動販売機シミュレーターを作ってみよう

複数のStateクラスに共通ルールを設けたい。Moo::Roleのrequiresでインターフェースを定義し、統一的なAPI設計を実現します。

@nqounetです。

前回は、状態ごとにクラスを作ることで、if/elseの肥大化問題を解決しました。

しかし、まだ問題が残っています。すべての状態クラスが同じメソッド(insert_coinselect_productdispense)を持つ必要がありますが、それを強制する仕組みがありません。

今回は、Moo::Roleを使って「共通の約束」を定義しましょう。

問題点を確認する

前回作った状態クラスを振り返ってみましょう。

WaitingStateもCoinInsertedStateも、同じ3つのメソッドを持っています。

  • insert_coin
  • select_product
  • dispense

しかし、もし新しい状態クラスを作るときに、うっかりメソッドを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クラスを作成します。お楽しみに!

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