仮想サーファーの波乗り

仮想化エンジニアの日常

プログラミング・ブロックチェーン・VR/AR・作業効率化・中国事情などに関してよく書きます。

【番外編】PythonでSlackBot開発 リファクタリングしてたらハマったこと


PythonのSlackBot開発でリファクタリングを進めていたら、色々ハマったところとその解消方法をメモしておきます。

ファイル間のモジュールの受け渡しや、LocalとHerokuでの変数の読み込みでどっぷりとハマることが多く時間を費やしてしまいました。悔しい。


そうだ、リファクタリングしよう!

昨日まで可読性や拡張性は無視して、ノリと勢いだけで書き続けていた日々。

github.com

↑ こちらのライブラリを利用させていただいてここまでslackBotを実装してきましたが、「run.py」というファイルに処理のすべてを書いていてメソッド切り出しもほとんどしていないというクソコードの極み状態になっていました。


(´-`).。oO(今後様々な機能を拡張させていくうえで、このままだと辛すぎる...。


ていうことで、Pythonの基本的なデザインパターンを元にリファクタリングを進めていくことにしました。その過程で詰まって時間を無駄にしたこととその解消方法を書いておきます。


PythonでのMVTモデル

リファクタリングを始めるにあたって、どんなファイル構成にしようかなと。Javaとかでよくあるシステム構成は ↓ こんな感じのイメージだと思います。

f:id:virtual-surfer:20180408145140p:plain

【アプリケーションアクセス→処理→データベースアクセス→レスポンスの流れ】
・ブラウザなどからGET・POSTデータを受け付ける。
・Controllerでそのアクセスをハンドリング。データアクセスが必要なければ、既存Viewページを返す。
・条件分岐を伴って処理が必要な場合は、Serviceを呼び出して処理を行う。
・データアクセスが必要な場合は、Repositoryを呼び出し、データベースに渡すためのEntityを生成。
・データベースに参照・更新アクセスを行い、返ってきたデータをServiceに返す。
・ブラウザに返す必要があれば、コントローラーがViewファイルとデータを組み合わせてブラウザにレスポンスを返す。

基本的にはこのような仕組みになっている。


これがPythonのMVTモデルだと、↓ こうなる。

f:id:virtual-surfer:20180408145924p:plain


これまではRailsのMVCモデルに慣れていたので、MVTモデルという初めて聞くデザインパターンに目新しいものがあるのかと期待しましたが、名前が違うだけで役割分けはそんなに変わらないのかな。JavaのSpringフレームワークではControllerという名前のものがViewになっているのは少し違和感がありますが、そういうもんなんでしょうね。

今回のリファクタリングでは、上記MVTモデルを参考にしつつJavaのデザインパターンに引っ張られて以下のようなファイル構成になりました。

botアプリケーションのファイル構成

bot
.
├── entity
│   ├── __init__.py
│   └── models.py
├── repository
│   ├── __init__.py
│   ├── twitter_user_add.py
│   └── twitter_user_delete.py
├── service
│   ├── __init__.py
│   ├── common_service.py
│   ├── docomo_dialogue_service.py
│   ├── slackbot_service.py
│   └── twitter_service.py
├── template
│   ├── __init__.py
│   └── slackbot_template.py
├── view
├── .buildpacks
├── .env
├── .gitignore
├── Procfile
├── requirements.txt
├── run.py
├── runtime.txt
└── slackbot_settings.py

...色々とつっこみどころがありそうなファイル構成ですが、なにぶん正解を知らないので...。作っていく過程で不便だなと思ったらファイル構成は改善していこうという方針でやっていきます。


以上のようなファイル構成にしてSlackBotを動かそうとしたら見事にハマってbotに話しかけても返答をしてくれないようになってしまいました。


SlackBotリファクタリングで詰まったことと解消方法

別ファイルの処理呼び出しにはimport必須問題

Botの処理の呼び出し順としては、「run.py」ファイルを起動することで、Botを起動する。そこからSlackでメンションが飛んでくれば、「slackbot_template.py」でメッセージを受け付けて、「slackbot_service.py」で処理を受け付け、「docomo_dialogue_service.py」でDocomoAPIを叩いてレスポンスを返してSlackに送信!!...というものにしたいけど、動かない...。動かない状態の各ファイルの状態は以下。

run.py

from slackbot.bot import Bot

def main():
    bot = Bot()
    bot.run()

if __name__ == '__main__':
    main()

slackbot_template.py

from slackbot.bot import default_reply
from service import slackbot_service

@default_reply(matchstr='(.*)')
def talk(message, input):
    slackbot_service.dialogue_with_docomo_api(message, input)

slackbot_service.py

# coding=utf-8
from service import twitter_service, common_service, docomo_dialogue_service

def dialogue_with_docomo_api(message, input):
    text = docomo_dialogue_service.create_response_with_context(input).get('utt')
    reply_on_slack(message, text)

def reply_on_slack(message, text):
    """
    Slackでメッセージを送ってきたユーザーに対してメンションをつけてtext返信
    """
    message.reply(text)

docomo_dialogue_service.py

# coding=utf-8
import os
import requests
import json

context = {}

def create_response_with_context(input):
    global context
    context_key = 'context'
    url = 'https://api.apigw.smt.docomo.ne.jp/dialogue/v1/dialogue?APIKEY={}'.format(os.environ['DOCOMO_API_KEY'])
    headers = {'Content-type': 'application/json'}
    data = {
        'utt': input,
        'context': context.get(context_key, ''),
        'mode': 'dialog',
        'place': '東京'
    }
    response = requests.post(
        url,
        data=json.dumps(data),
        headers=headers
    ).json()
    context[context_key] = response['context']
    return response

「$ forego run python run.py」で実行して話しかけてみると...

f:id:virtual-surfer:20180408152119p:plain

デフォルトの回答っぽいレスポンスが返ってきて、DocomoAPIを叩いてくれていません...。


結構悩みましたが、シンプルに呼び出したいファイルを「run.py」でimportしていないのが原因でした...。何やってんねんという感じですが、一度ハマると気づかないのが悲しい。「run.py」を以下のように編集して再度ファイル実行して話しかけてみます。

run.py

from slackbot.bot import Bot
from template import slackbot_template

def main():
    bot = Bot()
    bot.run()

if __name__ == '__main__':
    main()

f:id:virtual-surfer:20180408152501p:plain

きたー!雑談できてない気もしますが、DocomoAPIでレスポンスを返してくれるようになりました!ファイル間の処理の呼び出しにはimportしないといけないよって話ですね。学びました。


HerokuとLocalでAPI_TOKENの変数名違うとAttributeError: module 'x' has no attribute 'x'問題

これも見事にハマって無駄に時間を費やしてしまいました。

LOCALではAPI_TOKENという変数名で「.env」ファイルから参照、HerokuではSLACKBOT_API_TOKENという変数名で値を設定していました。その結果、「slackbot_settings.py」に書いている内容をHerokuが参照しようとしてしまうのか、AttributeError: module 'API_TOKEN' has no attribute 'API_TOKEN'とエラーになってしまう問題に悩みました。


結果としては、.envファイルに記載している「API_TOKEN」という変数名をHeroku上で設定している変数名「SLACKBOT_API_TOKEN」に変更して解決しました。まじ変数名大事。


まとめ

今回はリファクタリングばっかりして機能的にはほとんど前進がなかったですが、ベタ書きで特定の機能でしか利用できないような処理になってしまっていたのをメソッドに切り出したことで、「引数を変える」「メソッドを組み合わせる」だけで処理の応用をしやすくなりました。これで楽しく機能拡張していけそうです。

次回は、自動でプログラムを定期実行できるようにしていきたいと思います。


では!