Featured image of post 実行結果を Slack 返信・監査ログ・メトリクスに流したい。通知を Observer に任せる【Observer】

実行結果を Slack 返信・監査ログ・メトリクスに流したい。通知を Observer に任せる【Observer】

コマンドの実行後に起きる処理を、Command に書かず Observer に分ける。Mediator を通知元にして、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
システム開発・AIワークフローのご相談は Meetsource へ
Hugo で構築されています。
テーマ Stack は Jimmy によって設計されています。