Featured image of post 第6回:実行結果を通知したい【PerlでSlackボット指令センターを作る】

第6回:実行結果を通知したい【PerlでSlackボット指令センターを作る】

前回はMediatorパターンを使って、コマンドのルーティングを一元化しました。 今回は、ボットの「アウトプット」に注目します。

コマンドを実行した結果、何が起きるべきでしょうか?

  1. Slackに返信メッセージを送る(必須)
  2. 監査ログとしてファイルに記録する(セキュリティ要件)
  3. 実行時間を計測して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を「監視可能(Subject)」にする

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;
comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。