Featured image of post 第3回-通知を受け取るクラスを作ろう - Perlでローグライク通知システムを作ろう

第3回-通知を受け取るクラスを作ろう - Perlでローグライク通知システムを作ろう

ログと実績をそれぞれ独立したクラスに分離。通知を受け取るupdateメソッドを持たせ、責務分離の基礎を学びます。PerlとMooで実装。

@nqounetです。

「Perlでローグライク通知システムを作ろう」シリーズの第3回です。前回は、ログと実績が混在した問題のあるコードを見ました。今回は、通知を受け取るクラスを独立させて、責務を分離します。

前回の振り返り

前回は、実績システムを追加しようとしたときに以下の問題が発生しました。

  • メソッドの肥大化
  • 機能追加のたびに既存コード修正が必要
  • テストが困難

解決策:通知を受け取るクラスを分離する

問題を解決するために、以下のアプローチを取ります。

  1. ログ出力を専門に行うLogObserverクラスを作成
  2. 実績管理を専門に行うAchievementObserverクラスを作成
  3. 各クラスに「通知を受け取る」メソッド(update)を持たせる

LogObserverクラスを作成する

まずは、ログ出力専用のクラスを作成します。

 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
#!/usr/bin/env perl
# 言語: perl
# バージョン: 5.36以上
# 依存: Moo(cpanmでインストール)

use v5.36;

# ゲームイベントを表現するクラス
package GameEvent {
    use Moo;
    use v5.36;

    has 'type' => (
        is       => 'ro',
        required => 1,
    );

    has 'message' => (
        is       => 'ro',
        required => 1,
    );

    has 'timestamp' => (
        is      => 'ro',
        default => sub { time() },
    );
}

# ログ出力を担当するクラス
package LogObserver {
    use Moo;
    use v5.36;

    has 'prefix' => (
        is      => 'ro',
        default => '[LOG]',
    );

    # 通知を受け取るメソッド
    sub update ($self, $event) {
        say $self->prefix . " " . $event->message;
    }
}

# 使用例
package main {
    use v5.36;

    my $log_observer = LogObserver->new();

    my $event = GameEvent->new(
        type    => 'enemy_defeated',
        message => 'スライムを倒した!',
    );

    # 通知を送る
    $log_observer->update($event);
}

実行結果は以下のようになります。

1
[LOG] スライムを倒した!

LogObserverクラスは「ログを出力する」という1つの責務だけを持っています。

AchievementObserverクラスを作成する

次に、実績管理専用のクラスを作成します。

  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
#!/usr/bin/env perl
# 言語: perl
# バージョン: 5.36以上
# 依存: Moo(cpanmでインストール)

use v5.36;

package GameEvent {
    use Moo;
    use v5.36;

    has 'type' => (
        is       => 'ro',
        required => 1,
    );

    has 'message' => (
        is       => 'ro',
        required => 1,
    );

    has 'timestamp' => (
        is      => 'ro',
        default => sub { time() },
    );

    # イベントに関連するデータ(例:倒した敵の数、新しいレベルなど)
    has 'data' => (
        is      => 'ro',
        default => sub { {} },
    );
}

# 実績管理を担当するクラス
package AchievementObserver {
    use Moo;
    use v5.36;

    # 解除した実績のリスト
    has 'unlocked' => (
        is      => 'ro',
        default => sub { [] },
    );

    # 撃破数を記録
    has 'defeated_count' => (
        is      => 'rw',
        default => 0,
    );

    # 通知を受け取るメソッド
    sub update ($self, $event) {
        my $type = $event->type;

        if ($type eq 'enemy_defeated') {
            my $count = $self->defeated_count + 1;
            $self->defeated_count($count);

            if ($count == 1) {
                $self->_unlock('はじめての勝利');
            }
            if ($count == 10) {
                $self->_unlock('ハンター');
            }
        }
        elsif ($type eq 'level_up') {
            my $level = $event->data->{level} // 0;
            if ($level == 5) {
                $self->_unlock('成長');
            }
        }
    }

    # 実績を解除する(内部メソッド)
    sub _unlock ($self, $name) {
        push @{$self->unlocked}, $name;
        say "[ACHIEVEMENT] 実績解除: $name";
    }
}

# 使用例
package main {
    use v5.36;

    my $achievement_observer = AchievementObserver->new();

    # 敵を倒したイベント
    my $event1 = GameEvent->new(
        type    => 'enemy_defeated',
        message => 'スライムを倒した!',
    );
    $achievement_observer->update($event1);

    # レベルアップイベント
    my $event2 = GameEvent->new(
        type    => 'level_up',
        message => 'レベルが5になった!',
        data    => { level => 5 },
    );
    $achievement_observer->update($event2);

    say "";
    say "=== 解除した実績 ===";
    say "- $_" for @{$achievement_observer->unlocked};
}

実行結果は以下のようになります。

1
2
3
4
5
6
[ACHIEVEMENT] 実績解除: はじめての勝利
[ACHIEVEMENT] 実績解除: 成長

=== 解除した実績 ===
- はじめての勝利
- 成長

AchievementObserverクラスは「実績を管理する」という1つの責務だけを持っています。

両方のObserverを組み合わせる

2つのObserverを組み合わせて、ゲームイベントを両方に通知してみましょう。

  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
#!/usr/bin/env perl
# 言語: perl
# バージョン: 5.36以上
# 依存: Moo(cpanmでインストール)

use v5.36;

package GameEvent {
    use Moo;
    use v5.36;

    has 'type' => (
        is       => 'ro',
        required => 1,
    );

    has 'message' => (
        is       => 'ro',
        required => 1,
    );

    has 'timestamp' => (
        is      => 'ro',
        default => sub { time() },
    );

    has 'data' => (
        is      => 'ro',
        default => sub { {} },
    );
}

package LogObserver {
    use Moo;
    use v5.36;

    has 'prefix' => (
        is      => 'ro',
        default => '[LOG]',
    );

    sub update ($self, $event) {
        say $self->prefix . " " . $event->message;
    }
}

package AchievementObserver {
    use Moo;
    use v5.36;

    has 'unlocked' => (
        is      => 'ro',
        default => sub { [] },
    );

    has 'defeated_count' => (
        is      => 'rw',
        default => 0,
    );

    sub update ($self, $event) {
        my $type = $event->type;

        if ($type eq 'enemy_defeated') {
            my $count = $self->defeated_count + 1;
            $self->defeated_count($count);

            if ($count == 1) {
                $self->_unlock('はじめての勝利');
            }
            if ($count == 10) {
                $self->_unlock('ハンター');
            }
        }
        elsif ($type eq 'level_up') {
            my $level = $event->data->{level} // 0;
            if ($level == 5) {
                $self->_unlock('成長');
            }
        }
    }

    sub _unlock ($self, $name) {
        push @{$self->unlocked}, $name;
        say "[ACHIEVEMENT] 実績解除: $name";
    }
}

package main {
    use v5.36;

    # Observerを作成
    my $log_observer = LogObserver->new();
    my $achievement_observer = AchievementObserver->new();

    # イベントを作成して両方に通知
    my $event = GameEvent->new(
        type    => 'enemy_defeated',
        message => 'スライムを倒した!',
    );

    # 各Observerに通知
    $log_observer->update($event);
    $achievement_observer->update($event);
}

実行結果は以下のようになります。

1
2
[LOG] スライムを倒した!
[ACHIEVEMENT] 実績解除: はじめての勝利

ログ出力と実績管理が、それぞれ独立したクラスで行われています。

改善された点

クラスを分離したことで、以下の点が改善されました。

問題改善前改善後
メソッドの肥大化1つのメソッドに複数の処理各Observerが1つの責務を持つ
機能追加既存メソッドを修正新しいObserverクラスを追加
テスト分離してテスト困難各Observerを個別にテスト可能

共通点:updateメソッド

LogObserverAchievementObserverには共通点があります。

  • 両方ともupdateという名前のメソッドを持つ
  • updateメソッドはGameEventを受け取る
  • 受け取ったイベントに対して適切な処理を行う

この「updateメソッドを持つ」という約束を、次回は正式なルールとして定義します。

今回のまとめ

今回は、通知を受け取るクラスを分離しました。

  • LogObserver: ログ出力を担当
  • AchievementObserver: 実績管理を担当
  • 両クラスともupdateメソッドで通知を受け取る

完成コード

今回の完成コードは以下の通りです。

  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
#!/usr/bin/env perl
# 言語: perl
# バージョン: 5.36以上
# 依存: Moo(cpanmでインストール)

use v5.36;

package GameEvent {
    use Moo;
    use v5.36;

    has 'type' => (
        is       => 'ro',
        required => 1,
    );

    has 'message' => (
        is       => 'ro',
        required => 1,
    );

    has 'timestamp' => (
        is      => 'ro',
        default => sub { time() },
    );

    has 'data' => (
        is      => 'ro',
        default => sub { {} },
    );
}

package LogObserver {
    use Moo;
    use v5.36;

    has 'prefix' => (
        is      => 'ro',
        default => '[LOG]',
    );

    sub update ($self, $event) {
        say $self->prefix . " " . $event->message;
    }
}

package AchievementObserver {
    use Moo;
    use v5.36;

    has 'unlocked' => (
        is      => 'ro',
        default => sub { [] },
    );

    has 'defeated_count' => (
        is      => 'rw',
        default => 0,
    );

    sub update ($self, $event) {
        my $type = $event->type;

        if ($type eq 'enemy_defeated') {
            my $count = $self->defeated_count + 1;
            $self->defeated_count($count);

            if ($count == 1) {
                $self->_unlock('はじめての勝利');
            }
            if ($count == 10) {
                $self->_unlock('ハンター');
            }
        }
        elsif ($type eq 'level_up') {
            my $level = $event->data->{level} // 0;
            if ($level == 5) {
                $self->_unlock('成長');
            }
        }
    }

    sub _unlock ($self, $name) {
        push @{$self->unlocked}, $name;
        say "[ACHIEVEMENT] 実績解除: $name";
    }
}

package main {
    use v5.36;

    my $log_observer = LogObserver->new();
    my $achievement_observer = AchievementObserver->new();

    my @events = (
        GameEvent->new(
            type    => 'enemy_defeated',
            message => 'スライムを倒した!',
        ),
        GameEvent->new(
            type    => 'item_acquired',
            message => '薬草を手に入れた!',
        ),
        GameEvent->new(
            type    => 'level_up',
            message => 'レベルが5になった!',
            data    => { level => 5 },
        ),
    );

    say "=== ダンジョン探索 ===";
    say "";

    for my $event (@events) {
        $log_observer->update($event);
        $achievement_observer->update($event);
    }

    say "";
    say "=== 解除した実績 ===";
    say "- $_" for @{$achievement_observer->unlocked};
}

次回予告

次回は「通知を受け取る約束を決めよう」です。Moo::Rolerequiresを使って、「全てのObserverはupdateメソッドを持つ」という約束を正式に定義します。

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