Featured image of post 第3回-状態ごとに専用クラスを作ろう - Mooを使って自動販売機シミュレーターを作ってみよう

第3回-状態ごとに専用クラスを作ろう - Mooを使って自動販売機シミュレーターを作ってみよう

肥大化したif/elseをスッキリさせたい!状態ごとにクラスを分離して、単一責任の原則を実践。MooでStateクラスを作成します。

@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_productdispenseは自分自身を返す(状態が変わらない)
  • 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を使ってすべての状態クラスに共通のインターフェースを定義します。お楽しみに!

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