5000164 is here

Scala で Slack bot

Published 2017.11.29 by 菅原 浩

動機

Scala の勉強をするにあたって題材をどうしようか考えた。
Twitter のことを最近いいなと感じ始めていたのだけれど投稿するのが面倒くさくて、簡単に投稿できるようにしたかったので普段よく使っている Slack から投稿できるようにすることにした。

概要

Slack の bot に投げた内容をツイートする。

コード

ここです。

5000164/scala-bot

技術的なところ

Scala のライブラリと Java のライブラリ

build.sbt で slack-scala-client は追加できるのに twitter4j-core は追加できないと悩んでいたら、 % と %% が違うことを知った。

groupID % artifactID % revision のかわりに、 groupID %% artifactID % revision を使うと(違いは groupID の後ろの二つ連なった %%)、 sbt はプロジェクトの Scala のバイナリバージョンをアーティファクト名に追加する。 これはただの略記法なので %% 無しで書くこともできる:
sbt Reference Manual — ライブラリ依存性

slack-scala-client は Scala のライブラリなので略記法でいける、 twitter4j-core は Java のライブラリなので略記法ではいけない、ということだった。

モックを使ったテスト

コントローラーのテストを書く時に、副作用が出る部分をモックにしようとした。
調べたら Mockito というモックライブラリがよく使われているようなので、 ScalaTest と Mockito を使うことにした。

情報をうまく探すことが出来なくて動かすまでに少し時間がかかった。

1
class OperatorSpec extends FreeSpec with MockitoSugar {

のように MockitoSugar をミックスインしたり、

1
import org.mockito.Mockito.{never, verify, when}

のように verify をインポートしたりすることで動くようになった。

しかし、モックの返り値を設定する thenReturn に

1
when(mockTwitter.tweet("text")).thenReturn(Right(Unit))

のように Unit を渡すとなぜかコンパイルエラーになってしまい動かなくなったので、

1
when(mockTwitter.tweet("text")).thenReturn(Right())

のように書いている。

また、

1
verify(mockTwitter).tweet("text")

はちゃんと引数の検証ができて想定していない値が渡されたらテストがコケるのに対して、

1
verify(mockClient).sendMessage("channel", "message")

の方は引数の値に関わらず、関数が実行されていればテストが通ってしまう状態になっている。
関数が実行されていなければテストはコケる。

このあたりのコードはここに書いてある。

scala-bot/OperatorSpec.scala at master · 5000164/scala-bot

副作用がある関数の返り値

今回の場合では副作用は Twitter にツイートすることで発生する。
副作用がある関数の返り値をどうやって表現するか、 Boolean にするか Option にするかライブラリが投げるエラーをそのままキャッチするか、どれもしっくりこないと思っていて調べていたら Either を見つけたので使ってみた。
Either だと、エラーが起きたら文字列を返して、正常終了したら何も返さない、ということが表現できた。
ツイートに成功したらそのままで、ツイートに失敗したら失敗した旨を Slack に投げたいと思っていたので、

1
2
3
4
twitter.tweet(Command.content(message.text)) match {
  case Right(_) =>
  case Left(error_message) => client.sendMessage(message.channel, error_message)
}

のように表現することができた。

コマンドを増やしやすいような設計

今はツイートする機能だけだが、今後機能を増やしたいと思った時に追加しやすいように心がけた。
そのために DDD を意識しながら全体の設計を行った。
DDD はまだ全然勉強できておらず、これからたくさん学ぶ必要があるという感じだが、責任やレイヤーというものを意識した。

インターフェイス層としては

というように分け、ドメイン層としては

というように分けた。
判定したコマンドは case object としてコントローラーに返し、コントローラー側でパターンマッチを行って実際に処理を行うコマンドに投げる、という形にすることで、コマンドが追加しやすいようにした。

1
2
3
4
5
// コマンドに応じて処理を行う
Command.dispatch(message.text) match {
  case Some(TweetCommand) => // コマンドの内容を書く
  // 新しいコマンドを追加する時はここに書いていけばいい
}

まとめ

Slack から Twitter に投稿できるようになって便利。
小さいものでも、動くものを作ることで勉強になった。
これからも勉強を続ける。