冥冥乃志

ソフトウェア開発会社でチームマネージャをしているエンジニアの雑記。アウトプットは少なめです。

follow us in feedly

Twitter4S 2.0.0をリリースしました

Version 1.0.0のリリース以来、しばらくRubyの勉強を始めたり仕事が忙しかったりでノータッチだったのですが、いい加減旧APIにしか対応していない状態もまずいので、先々月あたりからこっそり始めて*1昨日こっそりリリースしました。まあ、誰からも文句がなかったりしたあたり、誰も困ってない(=誰も使ってない)なのでしょうが、私の勉強的にはおいしいところがあったのでよしとしましょう。


リポジトリShinsuke-Abe/twitter4s · GitHub

リリースノート

Version 2.0.0では以下の対応を行っています。

Twitter API 1.1対応

ライブラリ利用者に直接関わる部分です。具体的にはTwitter4J 3.0.3のAPIメソッドのラップが完了し、Version 1.0.0の時と同じく、Twitter4SのメソッドがTwitter4Jと1対1になるようにメソッドの統廃合が行われています。

大きく変わった部分はListリソースのAPIです。Twitter4J 3系からリストの指定方法が、リストIDかユーザ指定情報*2+SLUG*3という形に選択肢が増えています。

そのため、Listリソースを扱うAPIのうち、対象リストを指定するメソッドシグニチャを変更しました。方向性としては1.0.0の頃からユーザ指定情報(User.isSpecifiedByメソッド)などで採用しているEitherを利用した指定方法です。例えば、Twitter4Jにおいて、showUserListは以下のようにオーバーロードされています。

public interface ListResources {
  // リストIDを指定
  UserList showUserList(int listId) throws TwitterException;

  // ユーザId + SLUGを指定
  UserList showUserList(long userId, String slug) throws TwitterException;

  // スクリーン名 + SLUGを指定
  UserList showUserList(String screenName, String slug) throws TwitterException;
}


Twitter4Sでは上記APIを以下のようにラップしています。

trait ListResources {
  def showUserList(listSpecificInfo: UesrList.SpecificInfo): UserList
}


UserList.SpecificInfoは、

Either[Int, (User.SpecificInfo, String)]

で、UserListオブジェクトのisSpecifiedByメソッドを利用して指定します。isSpecifiedByメソッドシグニチャは以下の通りです。

  def isSpecifiedBy(listId: Int): Either[Int, (User.SpecificInfo, String)]
  def isSpecifiedBy(listOwnerUser: UserSpecificInfo, slug: String): Either[Int, (User.SpecificInfo, String)]


なお、Twitter4J 2系からメソッド名が変更になったものについては、それまでのシグニチャのラップメソッドも残していますが、Twitter4Jと合わせて非推奨扱いとしています。これらのメソッドについては3系で追加された新しいリストの指定方式には対応していません。

ライブラリの実装構成の変更

現状、ライブラリ利用者には直接影響しない部分ですので、内部構造に興味のない方は読飛ばしても問題有りません。

Twitter4Sはパッケージ構成も含めてTwitter4Jの純粋なラップライブラリであるため、traitでリソースごとのシグニチャを定義し、それをmixinするTwitterクラスにその実装が集中している構造をしていました。

利用者の観点からすると、この構造は便利なのですが、ライブラリ作成者の観点からはちょっとした問題点がありました。メソッドシグニチャ変更によるコンパイルエラーがTwitterクラスに集中するため、段階的なメソッド対応を仕様にも、全てのコンパイルエラーに対して仮対応してコンパイルエラーがない状態にしないと、そもそもテストすら実行できないということです。

今後のメンテナンスも考えると実装を集中させたくない。ただし、Twitter4JのようにAPIが固まっている利点は維持したい。という2点を踏まえて、以下のように構造に変更しています。

こうすることで、テスト時には対象の実装traitのみmixinすることができるようになり、テストが少し容易になりました。以下に実装traitを例示します。

trait SpamReportingResourcesImpl extends SpamReportingResources {
  self: Twitter =>

  def reportSpam(specificUser: User.SpecificInfo): User = {
    require(specificUser != null)

    specificUser match {
      case Right(userId) => twitter4jObj.reportSpam(userId)
      case Left(screenName) => twitter4jObj.reportSpam(screenName)
    }
  }
}

実装されるメソッドはTwitter4JのTwitterクラスを呼びますが、そのクラスのインスタンスはTwitter4SのTwitterクラスのフィールドになっているため、実装traitには自分型アノテーションを指定してアクセスできるようにしています。

そしてテストでは、何もmixinしていないTwitterオブジェクトに対して実装traitをmixinすることによって、Twitter4JのAPI変更の影響を局所化して、テストの実行が容易になるようにしました。

今後の展開

今後、どういう対応をしていくかについてですが、まず大前提として以前天領倉敷Scalaで発表したTwitter4Sが目指すものの基本的なところは変わりません。

Twitter4Sが(だいたい)できた_tkscala1006 - Google スライド

その上で、以下のことについて少しずつ手を入れて行きたいと思っています。

配布方法を整備

現状、githubからソースをcloneして自分でビルドして依存関係をとれ、というマッチョ仕様です。この辺については、もう少しユーザフレンドリーでありたいとは思っています。mavenのセントラルリポジトリに登録すれば良いのでしょうが、ちょっとだけ面倒臭さが先に来てます。

DSLを作りたい

ここで、DSLと言っているのは「オレのオレによるオレのためのDSL」です。実装をtraitに切り離してmixinで操作するテストコードを書いているときに、構造がDCIっぽくなってきたのに味をしめたのが大きな理由。

Twitter4JのようにTwitter4Sを使いたい人に対して今までの使い勝手を提供しつつ、自分に取ってわかりやすいコードが書けるようなDSLをのせられないかなあ、というのが今思っていることです。まあ、そのためには読み直さなければならない本がいくつかある訳ですが。。。

テストにモックを使う

テストについては課題があります。実装traitに切り離すことで、テストが少し容易にはなりましたが、そもそもTwitter4SのテストはTwitter4Jのテストをなぞる形になっています。

Twitter4Sの関心事としては、正しくTwitter4JのAPIが呼ばれているかであって、実際にTwitter APIを呼ぶところはTwitter4Jに委譲している、つまりテストされている部分を改めてテストしている訳です。それでいて、実のところコール対象のTwitter4Jのメソッドが呼ばれているかどうかは正しく確認されていません。

それに加えて、テストが大きくなっている部分ではRate limitに引っかかって頻繁にテストが止まるということもあります。Twitter4Jを通じてTwitter APIのコールがされている時点で、Twitter4Sとしては関心事としてはある意味確認したいことが確認できている状態なのですが、テストコード的にはRedになる、という状況です。しかもRate limitに引っかかってしまうと、しばらくその状態が続きます。

この状況を改善するために、モックを使ってテストをするように書き換えようと思っています。Twitter APIが呼ばれる部分のテストはTwitter4Jで担保されているから、Twitteer4Sとしては正しくラップできていることの担保に集中しようということです。

*1:途中、メガテン4による中断期間が1ヶ月間あり

*2:ユーザIDまたはスクリーン名

*3:これ、日本語で表すとしたらなんでしょうねえ。。。