1. はじめに
本記事は、拙作【AWS・Python】Slash Commandsを拡張性高くより楽に実装するの実装をより楽に修正した物です。
できる物などは変わらずに、よりコード的にすっきりしたのと、実装の際のコストが減るので記事にしました。
2. 実装
2.1 以前の方法(Chain Of Responsibility)
以前、SlashCommand を実装したときは、GoF のデザインパターンの1つであるChain Of Responsibility
を利用しました。
詳細は、前記事に譲りますが、実装する際は以下の4つの手順を踏む必要がありました。
-
CommandExecutor
クラスを継承するクラスを新規作成する -
__init__(self)
にて、自身が受け付けるslash_command
文字列を付与 -
execute()
関数を実装する - 新規作成したクラスを、数珠つなぎに呼べるように
CommandExecutorRequestHandler
クラスを作成する
この方法には、追加が楽というメリットはありますが以下に挙げるようなデメリットもありました。
-
ChainOfResponsibility
の概念を覚える必要がある - SlashCommands を受け入れるコードを書くまでに4ステップ踏む必要がある
-
CommandExecutorRequestHandler
に新規作成したクラスを追加しないと動作しない -
CommandExecutorRequestHandler
に追加したクラスの順序によっては予期せぬ動作をする-
AllAccept
などの全ての処理を行うクラスを一番最初に追加すると、後続の処理は全て動かなくなる、など
-
-
ChainOfResponsibility
の性質上、条件分岐を複数回行うため実行時間が遅くなる
2.2 今回の方法(Decorator)
以前に対して、今回はDecorator
というのを利用します。
こちらも、GoF のデザインパターンで登場する名前と同じなのですが、
実際の処理はデザインパターンで登場するような処理とは異なります。
この方法を利用すると、上記のデメリットはほぼ全て解決されます。
2.2.1 コマンドを追加する処理
実際にコーディングが必要なのは以下の通りです。
コメントで、上記の手順と対応する番号を付与します。
@SlashCommand.add(command="/hoge") # 1, 2: commandがSlashCommandで受け付ける文字列
def hoge(params: dict): # 3: 処理関数の実装
"""
/hogeというコマンドを受け取って処理を行う
"""
return {
"response_type": "in_channel",
"text": f"hoge: {str(params)}"
}
@SlashCommand.add(command="/fuga") # 1, 2: commandがSlashCommandで受け付ける文字列
def fuga(params: dict): # 3: 処理関数の実装
"""
/fugaというコマンドを受け取って処理を行う
"""
return {
"response_type": "in_channel",
"text": f"fuga: {str(params)}"
}
# 4: 正確には4と対応しないが、数珠つなぎの部分と対応
SlashCommand.execute(params=<payload_from_slack>)
2.2.2 SlashCommand本体の実装
以下は、以前の実装方法でいうところのCommandExecutor
クラスです。
decorator で受け取った関数を、{command: 関数}
という dict で保持しておき、
SlashCommand.execute()
が呼ばれた際に辞書の文字列でアクセスしています。
class SlashCommand:
"""
Decoratorを利用して、処理の登録と実行を行っている
メッセージの読み取りを始め、処理の移譲をおこなう。
"""
executor_dict: dict = {}
guard = None
def __init__(self):
pass
@staticmethod
def _get_command_from(params: dict) -> str:
"""
paramsに入ったcommand文字列を取得
"""
if 'command' not in params:
raise Exception
if len(params['command']) == 0:
raise Exception
return params['command'][0]
@classmethod
def add(cls, command: str, guard=False):
"""
このdecorator()が受け取るfを登録しておく
"""
def decorator(f):
cls.executor_dict[command] = f
if guard:
cls.guard = f
return f
return decorator
@classmethod
def execute(cls, params: dict):
command = cls._get_command_from(params=params)
if command in cls.executor_dict:
return cls.executor_dict[command](params=params)
else:
if cls.guard is not None and callable(cls.guard):
return cls.guard()
else:
raise NotImplementedError
2.3 Chaliceを利用した実装例
最後にchalice
を利用した実装例を載せます。
デコレータを利用することで、すっきりとした見通しの良いコードになった気がします。
以前の実装方法のソースコードはここにあります。
(実装の都合上、/hoge
や/fuga
はchalicelib
というパッケージの中に含まれています)
import urllib.parse
from chalice import Chalice
app = Chalice(app_name='<your_project_name>')
# ==================
# このあたりに上記のコード
# ==================
class SlashCommand:
# 行数の関係で省略、上記と同じコードが入ります。
pass
@SlashCommand.add(command="/hoge") # 1, 2: commandがSlashCommandで受け付ける文字列
def accept_hoge(params: dict): # 3: 処理関数の実装
"""
/hogeというコマンドを受け取って処理を行う
"""
return {
"response_type": "in_channel",
"text": f"hoge: {str(params)}"
}
@SlashCommand.add(command="/fuga") # 1, 2: commandがSlashCommandで受け付ける文字列
def accept_fuga(params: dict): # 3: 処理関数の実装
"""
/fugaというコマンドを受け取って処理を行う
"""
return {
"response_type": "in_channel",
"text": f"fuga: {str(params)}"
}
@app.route('/slack/slash_commands', methods=['POST'], content_types=['application/x-www-form-urlencoded'])
def receive_slash_command():
request = app.current_request
if request.raw_body is None:
# 予期しない呼び出し。400 Bad Requestを返す
return {'statusCode': 400}
payload = urllib.parse.parse_qs(request.raw_body.decode('utf-8'))
return SlashCommand.execute(params=payload)
3. おわりに
以前までは、ChainOfResponsibility
ってかなり便利だなと思っていたのですが、
実装の手順が煩雑になったりとデメリットの方が最近は気になりました。
その点、decorator は、メタプログラミング、と名のつく通り、かなりいろいろなことができるので便利です。
(あまりに複雑なものは、後々の自分や他の人への可読性という意味では危ないですが)
この方法を利用すれば、SlashCommand の処理のみに集中できるので良いかと思います。