@nqounetです。
「Perlでローグライク通知システムを作ろう」シリーズの第7回です。前回は、Observerを動的に追加・削除する方法を学びました。今回は、間違ったオブジェクトがObserverとして登録されないよう、型チェックを追加します。
前回の振り返り
前回は、attachとdetachを使ってObserverを動的に管理する方法を学びました。
問題:間違ったオブジェクトを登録してしまう
現在のattachメソッドは、引数として何でも受け入れてしまいます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| # GameEventObserverではないオブジェクト
package NotAnObserver {
use Moo;
use v5.36;
# updateメソッドがない!
sub something_else ($self) {
say "何か別のこと";
}
}
# 間違えて登録
my $emitter = GameEventEmitter->new();
my $not_an_observer = NotAnObserver->new();
$emitter->attach($not_an_observer); # エラーにならない...
# 通知しようとすると...
$emitter->notify($event); # ここで初めてエラー!
|
通知する瞬間までエラーに気づけません。これは危険ですね。
解決策:does制約で型チェック
doesを使うと、「このRoleを適用しているか」をチェックできます。詳しくは「Mooで覚えるオブジェクト指向プログラミング」シリーズの第12回をご覧ください。
attachメソッドに型チェックを追加する
attachメソッドで引数をチェックするようにしましょう。
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
| #!/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 'data' => (
is => 'ro',
default => sub { {} },
);
}
package GameEventObserver {
use Moo::Role;
use v5.36;
requires 'update';
}
package LogObserver {
use Moo;
use v5.36;
with 'GameEventObserver';
sub update ($self, $event) {
say "[LOG] " . $event->message;
}
}
package NotAnObserver {
use Moo;
use v5.36;
# GameEventObserverではない!
sub something_else ($self) {
say "何か別のこと";
}
}
package GameEventEmitter {
use Moo;
use v5.36;
has 'observers' => (
is => 'ro',
default => sub { [] },
);
sub attach ($self, $observer) {
# 型チェック: GameEventObserverロールを適用しているか
unless ($observer->does('GameEventObserver')) {
die "Error: ObserverはGameEventObserverを実装している必要があります";
}
push @{$self->observers}, $observer;
say "[EMITTER] Observerを登録しました";
}
sub detach ($self, $observer) {
@{$self->observers} = grep { $_ != $observer } @{$self->observers};
}
sub notify ($self, $event) {
for my $observer (@{$self->observers}) {
$observer->update($event);
}
}
}
package main {
use v5.36;
my $emitter = GameEventEmitter->new();
say "=== 正しいObserverを登録 ===";
my $log_observer = LogObserver->new();
$emitter->attach($log_observer); # OK
say "";
say "=== 間違ったオブジェクトを登録しようとする ===";
my $not_an_observer = NotAnObserver->new();
# エラーをキャッチ
eval {
$emitter->attach($not_an_observer); # エラー!
};
if ($@) {
say "エラーをキャッチ: $@";
}
}
|
実行結果は以下のようになります。
1
2
3
4
5
| === 正しいObserverを登録 ===
[EMITTER] Observerを登録しました
=== 間違ったオブジェクトを登録しようとする ===
エラーをキャッチ: Error: ObserverはGameEventObserverを実装している必要があります at ...
|
間違ったオブジェクトを登録しようとすると、すぐにエラーになります。
Mooのisa制約を使う方法
別のアプローチとして、Mooのisa制約を使う方法もあります。observers属性の要素が全てGameEventObserverであることをチェックします。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| package GameEventEmitter {
use Moo;
use v5.36;
has 'observers' => (
is => 'ro',
default => sub { [] },
);
sub attach ($self, $observer) {
# isa制約で型チェック
die "Invalid observer" unless $observer->does('GameEventObserver');
push @{$self->observers}, $observer;
}
# ...
}
|
どちらの方法でも、間違ったオブジェクトの登録を防ぐことができます。
型チェックのメリット
型チェックを入れることで、以下のメリットがあります。
| メリット | 説明 |
|---|
| 早期エラー発見 | 登録時にエラーになるため、問題を即座に発見 |
| デバッグが容易 | 「どこで間違えたか」がすぐにわかる |
| コードの意図が明確 | 「GameEventObserverが必要」という意図が伝わる |
| 安全な拡張 | 新しいObserverを追加するときの指針になる |
循環参照への注意
ObserverがSubject(EventEmitter)への参照を持つ場合、循環参照によるメモリリークが発生する可能性があります。
詳しくは「PerlのScalar::Util::weaken完全ガイド」を参照してください。
本シリーズでは、ObserverがEventEmitterへの参照を持たない設計にしているため、循環参照の問題は発生しません。もしObserverがEventEmitterを参照する必要がある場合は、weak_ref => 1オプションを使用してください。
今回のまとめ
今回は、型チェックでバグを防ぐ方法を学びました。
doesメソッドでRoleの適用をチェックattach時に型をチェックして早期エラー発見- 循環参照への注意点
完成コード
今回の完成コードは以下の通りです。
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
| #!/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 'data' => (
is => 'ro',
default => sub { {} },
);
}
package GameEventObserver {
use Moo::Role;
use v5.36;
requires 'update';
}
package LogObserver {
use Moo;
use v5.36;
with 'GameEventObserver';
sub update ($self, $event) {
say "[LOG] " . $event->message;
}
}
package AchievementObserver {
use Moo;
use v5.36;
with 'GameEventObserver';
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('はじめての勝利');
}
}
}
sub _unlock ($self, $name) {
push @{$self->unlocked}, $name;
say "[ACHIEVEMENT] 実績解除: $name";
}
}
package SoundObserver {
use Moo;
use v5.36;
with 'GameEventObserver';
sub update ($self, $event) {
say "[SOUND] ♪ サウンドエフェクト再生";
}
}
package GameEventEmitter {
use Moo;
use v5.36;
has 'observers' => (
is => 'ro',
default => sub { [] },
);
sub attach ($self, $observer) {
unless ($observer->does('GameEventObserver')) {
die "Error: ObserverはGameEventObserverを実装している必要があります";
}
push @{$self->observers}, $observer;
}
sub detach ($self, $observer) {
@{$self->observers} = grep { $_ != $observer } @{$self->observers};
}
sub notify ($self, $event) {
for my $observer (@{$self->observers}) {
$observer->update($event);
}
}
}
package main {
use v5.36;
my $emitter = GameEventEmitter->new();
my $log_observer = LogObserver->new();
my $achievement_observer = AchievementObserver->new();
my $sound_observer = SoundObserver->new();
$emitter->attach($log_observer);
$emitter->attach($achievement_observer);
$emitter->attach($sound_observer);
say "=== ダンジョン探索 ===";
say "";
my $event = GameEvent->new(
type => 'enemy_defeated',
message => 'スライムを倒した!',
);
$emitter->notify($event);
}
|
次回予告
次回は「統計システムを追加しよう(OCP実践)」です。既存のコードを変更せずに新しいObserverを追加することで、開放閉鎖原則(OCP)を体感します。