前回は、コマンド分岐が増えすぎて巨大な if 文の塊(神オブジェクト)になってしまった惨劇を見ました。
今回は、この状況を解決するデザインパターン「Commandパターン」を導入します。
Commandパターンの本質は、「要求をオブジェクトとしてカプセル化すること」 です。
これにより、コマンドの追加が「既存コードへの追記」から「新しいクラスの作成」に変わり、コードの独立性が高まります。

Commandの役割を定義する
まず、すべてのコマンドが共通して持つべき振る舞いを Moo::Role で定義します。
今回のボットでは、以下の3つの機能が必要そうです。
- マッチング: メッセージが自分宛てか判断し、引数を抽出する (
match) - 実行: 実際に処理を行い、結果を返す (
execute) - 説明: ヘルプ表示用に自分の役割を説明する (
description)
1
2
3
4
5
6
7
8
9
| package Bot::Command::Role;
use Moo::Role;
use Types::Standard qw(Str);
requires 'match'; # 引数解釈(戻り値: 引数ハッシュリファレンス or undef)
requires 'execute'; # 処理実行(戻り値: 結果文字列)
requires 'description'; # ヘルプ用説明
1;
|
具体的なコマンドを作る
このロールを使って、各コマンドを独立したクラスとして実装します。
デプロイコマンド
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| package Bot::Command::Deploy;
use Moo;
with 'Bot::Command::Role';
sub match {
my ($self, $text) = @_;
if ($text =~ m{^/deploy\s+(\w+)(?:\s+(--force))?}) {
return { target => $1, force => $2 };
}
return undef;
}
sub execute {
my ($self, $args) = @_;
my $target = $args->{target};
my $force = $args->{force} ? "(強制)" : "";
return "🚀 $target 環境へのデプロイを開始しました$force...";
}
sub description { "/deploy <env> [--force] : 指定環境へデプロイします" }
1;
|
ログ取得コマンド
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| package Bot::Command::Log;
use Moo;
with 'Bot::Command::Role';
sub match {
my ($self, $text) = @_;
if ($text =~ m{^/log\s+(\w+)(?:\s+--lines\s+(\d+))?}) {
return { level => $1, lines => $2 // 10 };
}
return undef;
}
sub execute {
my ($self, $args) = @_;
return "📋 $args->{level} ログを直近 $args->{lines} 行取得しました...";
}
sub description { "/log <level> [--lines N] : ログを取得します" }
1;
|
このように、コマンドごとにファイル(クラス)が分かれます。正規表現やロジックが各クラスに閉じ込められ、お互いに干渉しなくなりました。
メインループの修正
さて、呼び出し側(ボット本体)はどう変わるでしょうか?
巨大な if-elsif は、すっきりとしたループに置き換わります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| use Bot::Command::Deploy;
use Bot::Command::Log;
my @commands = (
Bot::Command::Deploy->new,
Bot::Command::Log->new,
# 新しいコマンドはここに追加するだけ!
);
sub handle_message {
my ($text) = @_;
for my $cmd (@commands) {
if (my $args = $cmd->match($text)) {
return $cmd->execute($args);
}
}
return "不明なコマンドです。";
}
|
何が嬉しくなったのか?
- SRP(単一責任の原則)の遵守: パース処理と実行ロジックが各コマンドクラスに集約されました。
- OCP(開放閉鎖の原則)の遵守: 新しいコマンドを追加したいときは、新しいクラスを作って配列に追加するだけ。
handle_message のロジック自体をいじる必要はありません。 - テスト容易性:
Bot::Command::Deploy だけを単体テストすることが簡単になりました。
これでコードの見通しは劇的に良くなりました。
しかし、まだ課題が残っています。「管理者のみ実行可能」といった共通の権限チェックロジックや、コマンドが増えすぎた場合の検索効率の問題です。
次回は、これらのコマンドを賢く管理し、適切な場所に振り分ける「司令塔」として Mediatorパターン を導入します。
今回の完成コード
Commandパターンによって、ロジックがクラスごとに分割されました。
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/Command/Role.pm
package Bot::Command::Role;
use Moo::Role;
requires 'match';
requires 'execute';
requires 'description';
1;
# Bot/Command/Deploy.pm
package Bot::Command::Deploy;
use Moo;
with 'Bot::Command::Role';
sub match {
my ($self, $text) = @_;
if ($text =~ m{^/deploy\s+(\w+)(?:\s+(--force))?}) {
return { target => $1, force => $2 };
}
return undef;
}
sub execute {
my ($self, $args) = @_;
my $force = $args->{force} ? "(強制)" : "";
return "🚀 $args->{target} 環境へのデプロイを開始しました$force...";
}
sub description { "/deploy <env> [--force]" }
1;
# Bot/Command/Log.pm
package Bot::Command::Log;
use Moo;
with 'Bot::Command::Role';
sub match {
my ($self, $text) = @_;
if ($text =~ m{^/log\s+(\w+)(?:\s+(--lines\s+(\d+))?)?}) { # 正規表現修正
return { level => $1, lines => $3 // 10 };
}
return undef;
}
sub execute {
my ($self, $args) = @_;
return "📋 $args->{level} ログを直近 $args->{lines} 行取得しました...";
}
sub description { "/log <level> [--lines N]" }
1;
# main.pl (抜粋)
use Bot::Command::Deploy;
use Bot::Command::Log;
my @commands = (
Bot::Command::Deploy->new,
Bot::Command::Log->new,
);
sub handle_message {
my ($text) = @_;
for my $cmd (@commands) {
if (my $args = $cmd->match($text)) {
return $cmd->execute($args);
}
}
return "不明なコマンドです。";
}
|