前回はMediatorパターンを使って、コマンドのルーティングを一元化しました。
今回は、ボットの「アウトプット」に注目します。
コマンドを実行した結果、何が起きるべきでしょうか?
- Slackに返信メッセージを送る(必須)
- 監査ログとしてファイルに記録する(セキュリティ要件)
- 実行時間を計測してDatadogに送る(パフォーマンス監視)
これらをすべてCommandクラスの中に書くと、またしてもSRP(単一責任の原則)違反になります。
ここで役立つのが Observerパターン です。

Observerの役割
Observerパターンは、あるオブジェクト(Subject)の状態変化を監視し、変化があったときに他のオブジェクト(Observer)に自動的に通知する仕組みです。
今回のボットでは、Mediatorがコマンド実行完了(イベント発生)を通知し、複数のObserverがそれを受け取ってそれぞれの処理を行うという構成にします。
Observerロールの定義
まず、すべての監視者が守るべきインターフェースを定義します。
1
2
3
4
5
6
7
| package Bot::Observer::Role;
use Moo::Role;
# イベントを受け取るメソッド
requires 'update';
1;
|
具体的なObserverを作る
Slack通知用Observer
1
2
3
4
5
6
7
8
9
10
11
| package Bot::Observer::SlackNotifier;
use Moo;
with 'Bot::Observer::Role';
sub update {
my ($self, $event) = @_;
# 実際にはHTTP::TinyなどでSlack APIを叩く
print "[Slack通知] " . $event->{message} . "\n";
}
1;
|
監査ログ用Observer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| package Bot::Observer::FileLogger;
use Moo;
with 'Bot::Observer::Role';
use Time::Piece;
sub update {
my ($self, $event) = @_;
my $time = localtime->strftime('%Y-%m-%d %H:%M:%S');
my $log_line = sprintf "[%s] Command: %s, User: %s, Result: %s",
$time,
$event->{command_name},
$event->{user},
$event->{message};
print "[ファイルログ] $log_line\n";
# 実際には open $fh, '>>', 'audit.log' して書き込む
}
1;
|
Mediatorクラスを修正して、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
| package Bot::CommandMediator;
use Moo;
use Types::Standard qw(ArrayRef Object);
# コマンドリスト(前回と同じ)
has commands => ( ... );
# Observerリスト(追加)
has observers => (
is => 'ro',
isa => ArrayRef[Object],
default => sub { [] },
);
sub add_observer {
my ($self, $observer) = @_;
push @{$self->observers}, $observer;
}
sub notify_observers {
my ($self, $event) = @_;
for my $obs (@{$self->observers}) {
$obs->update($event);
}
}
sub dispatch {
my ($self, $text, $user_role, $user_name) = @_;
for my $cmd (@{$self->commands}) {
if (my $args = $cmd->match($text)) {
# 実行
my $result_msg = $cmd->execute($args);
# 結果をイベントとして全Observerに通知!
$self->notify_observers({
type => 'command_executed',
command_name => ref($cmd),
user => $user_name,
message => $result_msg,
args => $args,
});
return $result_msg;
}
}
# ...
}
|
疎結合の美しさ
これで、新しい通知先を追加したい場合も、ボット本体や既存のコードを一切変更する必要がなくなりました。
例えば、「エラー発生時にパトランプを回す」という要件が増えたら?
Bot::Observer::Patlite を作って、Mediatorに add_observer するだけです。
Command、Mediator、Observer。
3つの役者が揃いました。次回はこれらをすべて組み合わせた「統合コード」を見て、全体のデータの流れを確認しましょう。
イベント駆動アーキテクチャの縮図がここに完成します。
今回の完成コード
Observerパターン関連のクラスと、通知ロジックを追加したMediatorです。
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
| # Bot/Observer/Role.pm
package Bot::Observer::Role;
use Moo::Role;
requires 'update';
1;
# Bot/Observer/SlackNotifier.pm
package Bot::Observer::SlackNotifier;
use Moo;
with 'Bot::Observer::Role';
sub update {
my ($self, $event) = @_;
# 実際はSlack APIを使用
print "[Slack通知] " . ($event->{message} // '') . "\n";
}
1;
# Bot/CommandMediator.pm (更新版)
package Bot::CommandMediator;
use Moo;
use Types::Standard qw(ArrayRef Object);
has commands => ( is => 'ro', isa => ArrayRef[Object], default => sub { [] } );
has observers => ( is => 'ro', isa => ArrayRef[Object], default => sub { [] } );
sub register_command {
my ($self, $command) = @_;
push @{$self->commands}, $command;
}
sub add_observer {
my ($self, $observer) = @_;
push @{$self->observers}, $observer;
}
sub notify_observers {
my ($self, $event) = @_;
$_->update($event) for @{$self->observers};
}
sub dispatch {
my ($self, $text, $user_role, $user_name) = @_;
for my $cmd (@{$self->commands}) {
if (my $args = $cmd->match($text)) {
# 権限チェック等のロジック...
my $result_msg = $cmd->execute($args);
$self->notify_observers({
type => 'command_executed',
command_name => ref($cmd),
user => $user_name,
message => $result_msg,
args => $args,
});
return $result_msg;
}
}
return "不明なコマンドです。";
}
1;
|