0
0

More than 3 years have passed since last update.

【AWS・Python】Slash Commandsを拡張性高くより楽に実装する

Last updated at Posted at 2020-08-01

1. はじめに

本記事は、拙作【AWS・Python】Slash Commandsを拡張性高くより楽に実装するの実装をより楽に修正した物です。

できる物などは変わらずに、よりコード的にすっきりしたのと、実装の際のコストが減るので記事にしました。

2. 実装

2.1 以前の方法(Chain Of Responsibility)

以前、SlashCommand を実装したときは、GoF のデザインパターンの1つであるChain Of Responsibilityを利用しました。
詳細は、前記事に譲りますが、実装する際は以下の4つの手順を踏む必要がありました。

  1. CommandExecutorクラスを継承するクラスを新規作成する
  2. __init__(self)にて、自身が受け付けるslash_command文字列を付与
  3. execute()関数を実装する
  4. 新規作成したクラスを、数珠つなぎに呼べるようにCommandExecutorRequestHandlerクラスを作成する

この方法には、追加が楽というメリットはありますが以下に挙げるようなデメリットもありました。

  • ChainOfResponsibilityの概念を覚える必要がある
  • SlashCommands を受け入れるコードを書くまでに4ステップ踏む必要がある
  • CommandExecutorRequestHandlerに新規作成したクラスを追加しないと動作しない
  • CommandExecutorRequestHandlerに追加したクラスの順序によっては予期せぬ動作をする
    • AllAcceptなどの全ての処理を行うクラスを一番最初に追加すると、後続の処理は全て動かなくなる、など
  • ChainOfResponsibilityの性質上、条件分岐を複数回行うため実行時間が遅くなる

2.2 今回の方法(Decorator)

以前に対して、今回はDecoratorというのを利用します。
こちらも、GoF のデザインパターンで登場する名前と同じなのですが、
実際の処理はデザインパターンで登場するような処理とは異なります。

この方法を利用すると、上記のデメリットはほぼ全て解決されます。

2.2.1 コマンドを追加する処理

実際にコーディングが必要なのは以下の通りです。
コメントで、上記の手順と対応する番号を付与します。

example.py
@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()が呼ばれた際に辞書の文字列でアクセスしています。

slash_command.py
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/fugachalicelibというパッケージの中に含まれています)

app.py

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 の処理のみに集中できるので良いかと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0