目次


多忙な Java 開発者のための Scala ガイド

Scitter ライブラリーを機能強化する

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: 多忙な Java 開発者のための Scala ガイド

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:多忙な Java 開発者のための Scala ガイド

このシリーズの続きに乞うご期待。

Scala ファンの皆さん、「多忙な Java 開発者のための Scala ガイド」へようこそ。前回は、ソーシャル・ネットワーキング利用者の間で現在大きな関心を呼んでいるマイクロブロギング・サイト Twitter について説明しました。そして、Twitter の XML/REST ベースの API のおかげで、いかに Twitter が開発者にとって調査や実験の対象として興味深いものになっているかも説明しました。こうした説明をするために、前回は Twitter にアクセスするための Scala ライブラリーである、「Scitter」の基本構造を作り始めました。

Scitter の目標は以下のとおりです。

  • 単純に HTTP 接続を開いて作業を「手動で」行うよりも、はるかに容易に Twitter にアクセスできること。
  • Java クライアントからも容易に利用できること。
  • テスト用に容易にモックを作成できること。

この記事では Twitter の API すべてを完全に利用しているわけではありませんが、いくつかの中心的な API を利用し、後でこの Scitter ライブラリーをソース管理リポジトリーに置いて公開しておけば他の人達が残りの部分を容易に完成できるようにします。

これまでの復習: Scitter 0.1

前回の記事でどこまで説明したかを簡単に復習しましょう。

リスト 1. Scitter v0.1
package com.tedneward.scitter
{
  import org.apache.commons.httpclient._, auth._, methods._, params._
  import scala.xml._

  /**
   * Status message type. This will typically be the most common message type
   * sent back from Twitter (usually in some kind of collection form). Note
   * that all optional elements in the Status type are represented by the
   * Scala Option[T] type, since that's what it's there for.
   */
  abstract class Status
  {
    /**
     * Nested User type. This could be combined with the top-level User type,
     * if we decide later that it's OK for this to have a boatload of optional
     * elements, including the most-recently-posted status update (which is a
     * tad circular).
     */
    abstract class User
    {
      val id : Long
      val name : String
      val screenName : String
      val description : String
      val location : String
      val profileImageUrl : String
      val url : String
      val protectedUpdates : Boolean
      val followersCount : Int
    }
    /**
     * Object wrapper for transforming (format) into User instances.
     */
    object User
    {
      /*
      def fromAtom(node : Node) : Status =
      {
      
      }
      */
      /*
      def fromRss(node : Node) : Status =
      {
      
      }
      */
      def fromXml(node : Node) : User =
      {
        new User {
          val id = (node \ "id").text.toLong
          val name = (node \ "name").text
          val screenName = (node \ "screen_name").text
          val description = (node \ "description").text
          val location = (node \ "location").text
          val profileImageUrl = (node \ "profile_image_url").text
          val url = (node \ "url").text
          val protectedUpdates = (node \ "protected").text.toBoolean
          val followersCount = (node \ "followers_count").text.toInt
        }
      }
    }
  
    val createdAt : String
    val id : Long
    val text : String
    val source : String
    val truncated : Boolean
    val inReplyToStatusId : Option[Long]
    val inReplyToUserId : Option[Long]
    val favorited : Boolean
    val user : User
  }
  /**
   * Object wrapper for transforming (format) into Status instances.
   */
  object Status
  {
    /*
    def fromAtom(node : Node) : Status =
    {
    
    }
    */
    /*
    def fromRss(node : Node) : Status =
    {
    
    }
    */
    def fromXml(node : Node) : Status =
    {
      new Status {
        val createdAt = (node \ "created_at").text
        val id = (node \ "id").text.toLong
        val text = (node \ "text").text
        val source = (node \ "source").text
        val truncated = (node \ "truncated").text.toBoolean
        val inReplyToStatusId =
          if ((node \ "in_reply_to_status_id").text != "")
            Some((node \"in_reply_to_status_id").text.toLong)
          else
            None
        val inReplyToUserId = 
          if ((node \ "in_reply_to_user_id").text != "")
            Some((node \"in_reply_to_user_id").text.toLong)
          else
            None
        val favorited = (node \ "favorited").text.toBoolean
        val user = User.fromXml((node \ "user")(0))
      }
    }
  }


  /**
   * Object for consuming "non-specific" Twitter feeds, such as the public timeline.
   * Use this to do non-authenticated requests of Twitter feeds.
   */
  object Scitter
  {
    /**
     * Ping the server to see if it's up and running.
     *
     * Twitter docs say:
     * test
     * Returns the string "ok" in the requested format with a 200 OK HTTP status code.
     * URL: http://twitter.com/help/test.format
     * Formats: xml, json
     * Method(s): GET
     */
    def test : Boolean =
    {
      val client = new HttpClient()

      val method = new GetMethod("http://twitter.com/help/test.xml")

      method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, 
        new DefaultHttpMethodRetryHandler(3, false))

      client.executeMethod(method)
      
      val statusLine = method.getStatusLine()
      statusLine.getStatusCode() == 200
    }
    /**
     * Query the public timeline for the most recent statuses.
     *
     * Twitter docs say:
     * public_timeline
     * Returns the 20 most recent statuses from non-protected users who have set
     * a custom user icon.  Does not require authentication.  Note that the
     * public timeline is cached for 60 seconds so requesting it more often than
     * that is a waste of resources.
     * URL: http://twitter.com/statuses/public_timeline.format
     * Formats: xml, json, rss, atom
     * Method(s): GET
     * API limit: Not applicable
     * Returns: list of status elements     
     */
    def publicTimeline : List[Status] =
    {
      import scala.collection.mutable.ListBuffer
    
      val client = new HttpClient()

      val method = new GetMethod("http://twitter.com/statuses/public_timeline.xml")

      method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, 
        new DefaultHttpMethodRetryHandler(3, false))

      client.executeMethod(method)
      
      val statusLine = method.getStatusLine()
      if (statusLine.getStatusCode() == 200)
      {
        val responseXML =
          XML.loadString(method.getResponseBodyAsString())

        val statusListBuffer = new ListBuffer[Status]

        for (n <- (responseXML \\ "status").elements)
          statusListBuffer += (Status.fromXml(n))
        
        statusListBuffer.toList
      }
      else
      {
        Nil
      }
    }
  }
  /**
   * Class for consuming "authenticated user" Twitter APIs. Each instance is
   * thus "tied" to a particular authenticated user on Twitter, and will
   * behave accordingly (according to the Twitter API documentation).
   */
  class Scitter(username : String, password : String)
  {
    /**
     * Verify the user credentials against Twitter.
     *
     * Twitter docs say:
     * verify_credentials
     * Returns an HTTP 200 OK response code and a representation of the
     * requesting user if authentication was successful; returns a 401 status
     * code and an error message if not.  Use this method to test if supplied
     * user credentials are valid.
     * URL: http://twitter.com/account/verify_credentials.format
     * Formats: xml, json
     * Method(s): GET
     */
    def verifyCredentials : Boolean =
    {
      val client = new HttpClient()

      val method = new GetMethod("http://twitter.com/help/test.xml")

      method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, 
        new DefaultHttpMethodRetryHandler(3, false))
        
      client.getParams().setAuthenticationPreemptive(true)
      val creds = new UsernamePasswordCredentials(username, password)
      client.getState().setCredentials(
        new AuthScope("twitter.com", 80, AuthScope.ANY_REALM), creds)

      client.executeMethod(method)
      
      val statusLine = method.getStatusLine()
      statusLine.getStatusCode() == 200
    }
  }
}

このコードは少し長いですが、とても簡単に以下の 3 つの基本コンポーネントに分けることができます。

  • UserStatus という case クラスは、API 呼び出しへの応答として Twitter がクライアントに返送する基本型を表します。これらのクラスには、XML 表現を作成するためのメソッド、および XML 表現から抽出されるメソッドが含まれています。
  • Scitter シングルトン・オブジェクトは、認証ユーザーを必要としない操作を処理します。
  • Scitter インスタンスは (ユーザー名とパスワードをパラメーターとして)、認証ユーザーを必要とする操作を処理します。

この 2 つのタイプの Scitter の中で、これまでに説明した API は、test、verifyCredentials、そして public_timeline のみでした。これらの API は、(Apache HttpClient ライブラリーを使用する) 基本的な HTTP アクセスが機能することの検証や、XML レスポンスを Status オブジェクトに変換する基本的なフォームが機能することの検証には役立ちます。しかし、現状では「私の友達が何をつぶやいているのか」を照会するための、公開タイムラインに対する基本的なクエリーさえ実行することができず、さらにはコードベース内にある、DRY (Don't Repeat Yourself: 同じ処理のコードを繰り返し作成しないこと) 原則に反する問題を防ぐための基本的な手段すら講じていません。ましてや、ネットワークにアクセスするためのテスト用のモックをより簡単に作成できるようにする方法は検討すら始めていません。

この先が長いことは明らかです。

接続する

リスト 1 のコードで最も気になる点は、操作のシーケンスを繰り返している点です。つまり Scitter のオブジェクトとクラス両方のすべてのメソッドで、HttpClient インスタンスを作成し、初期化し、必要な認証パラメーターを設定し、等々を繰り返しています。Scitter のオブジェクトとクラスの間に 3 つのメソッドしかない場合には何とかなるかもしれませんが、これはとてもスケーラブルとは言えず、しかも操作が必要なメソッドは他にも大量にあります。さらに、後でこれらのメソッドに何らかのモックを追加したり、ローカルやオフラインでのテスト機能を追加したりするのは、非常に困難になります。そこで、この問題を修正しましょう。

ここで説明する内容は実際には Scala に特有の内容ではなく、単純に DRY の考え方を適用するだけにすぎません。そこで、まず基本的なオブジェクト指向の手法で始めることにし、実際の作業を行うためのヘルパー・メソッドを作成します。

リスト 2. コードベースに DRY 原則を適用する
package com.tedneward.scitter
{
  // ...
  object Scitter
  {
    // ...
    private[scitter] def execute(url : String) =
    {
      val client = new HttpClient()
      val method = new GetMethod(url)
      
      method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, 
        new DefaultHttpMethodRetryHandler(3, false))
        
      client.executeMethod(method)
      
      (method.getStatusLine().getStatusCode(), method.getResponseBodyAsString())
    }
  }
}

リスト 2 のコードを見ると、いくつかのことに気付くはずです。第 1 に、execute() メソッドからタプルを返しており、このタプルにステータス・コードとレスポンス本体の両方が含まれています。これは、タプルが言語の一部として組み込まれていることの強力さを示す一例です。これによって実質的に、1 つのメソッドを呼び出すだけで容易に複数の戻り値を得ることができるからです。もちろん Java コードでも、タプル要素を含む最上位レベルのクラスまたはネストされたクラスを作成すれば、同じことができます。しかしそうすると、この特定の 1 つのメソッド専用に大量のコードが必要になります。あるいは String キーと Object 値を持つ Map を返すこともできますが、そうするとタイプ・セーフが大きく損なわれます。タプルは劇的な効果を持つ機能ではありませんが、Scala が持つ便利な機能の 1 つであり、こうした機能があることで Scala は強力な言語となっています。

ここではタプルを使用しているので、Scala での別の構文的なイディオムを使って両方の結果をローカル変数の中に取り込むようにします。そのように書き直したバージョンの Scitter.test が以下のリスト 3 です。

リスト 3. DRY 原則に従った Scitter.test
package com.tedneward.scitter
{
  // ...
  object Scitter
  {
    /**
     * Ping the server to see if it's up and running.
     *
     * Twitter docs say:
     * test
     * Returns the string "ok" in the requested format with a 200 OK HTTP status code.
     * URL: http://twitter.com/help/test.format
     * Formats: xml, json
     * Method(s): GET
     */
    def test : Boolean =
    {
      val (statusCode, statusBody) =
        execute("http://twitter.com/statuses/public_timeline.xml")
      statusCode == 200
    }
  }
}

実際、2 番目のパラメーターは使わないので (test にはこの関数から値を返すコードがありません)、statusBody を完全に削除し、_ で置き換えることもできますが、他の呼び出し用に statusBody が必要なので、ここではそのままサンプルとして残すことにします。

execute() を見ても実際の HTTP 通信の処理に関する詳細がまったくわからないことに注目してください。これはカプセル化の基本です。こうすることで、後で execute() を別の実装に容易に変更することができます (後ほど、この変更を行います)。あるいは、新しい HttpClient オブジェクトを毎回インスタンス化せず 1 つの HttpClient オブジェクトを再利用することでコードを最適化することもできます。

次に、execute() メソッドが Scitter オブジェクトに適用されていることに気付いたでしょうか。これは、さまざまな Scitter インスタンスから execute() メソッドを使用できるということです (少なくとも今は使用することができます。ただし execute() の中で余計なことをすると使用できなくなります)。execute()private[scitter] にした理由はここにあります。こうすることで、com.tedneward.scitter パッケージの中にあるすべてのものが execute() を見られるようになります。

(もし皆さんがまだテストを実行していなかったら、ここでテストを実行し、すべてのものがまだ適切に動作することを確認してください。私はコードの説明に合わせて皆さんがテストを行うものと想定しています。そのため、私が特に言わなかったからといって、テストが必要ないという意味ではないことに注意してください。)

ところで、Scitter クラスをサポートするためには、認証アクセスのためのユーザー名とパスワードが必要です。そこで、さらに 2 つの String をパラメーターに取る、execute() メソッドのオーバーロードを作成することにします。

リスト 4. さらに DRY 原則を徹底したバージョン
package com.tedneward.scitter
{
  // ...
  object Scitter
  {
    // ...
    private[scitter] def execute(url : String, username : String, password : String) =
    {
      val client = new HttpClient()
      val method = new GetMethod(url)
      
      method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, 
        new DefaultHttpMethodRetryHandler(3, false))
        
	  client.getParams().setAuthenticationPreemptive(true)
	  client.getState().setCredentials(
		new AuthScope("twitter.com", 80, AuthScope.ANY_REALM),
		  new UsernamePasswordCredentials(username, password))
      
      client.executeMethod(method)
      
      (method.getStatusLine().getStatusCode(), method.getResponseBodyAsString())
    }
  }
}

実際のところ、この 2 つの execute() が認証以外の部分ではほとんど同じ処理をすることを考えると、2 番目の execute() の観点から最初の execute() を完全に作り直すことができます。ただしその場合、Scala ではオーバーロードされた execute() の戻り型が明示的でなければならないことに注意する必要があります。

リスト 5. 最大限 DRY 原則を適用する
package com.tedneward.scitter
{
  // ...
  object Scitter
  {
    // ...
    private[scitter] def execute(url : String) : (Int, String) =
	  execute(url, "", "")
    private[scitter] def execute(url : String, username : String, password : String) =
    {
      val client = new HttpClient()
      val method = new GetMethod(url)
      
      method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, 
        new DefaultHttpMethodRetryHandler(3, false))
        
      if ((username != "") && (password != ""))
      {
        client.getParams().setAuthenticationPreemptive(true)
        client.getState().setCredentials(
          new AuthScope("twitter.com", 80, AuthScope.ANY_REALM),
            new UsernamePasswordCredentials(username, password))
      }
      
      client.executeMethod(method)
      
      (method.getStatusLine().getStatusCode(), method.getResponseBodyAsString())
    }
  }
}

ここまでは順調です。Scitter コードの通信部分に DRY 原則を適用しました。そこで次の項目に進み、友達のつぶやきのリストを取得しましょう。

(友達に) 接続する

Twitter の API の説明には、friends_timeline という API を呼び出すと、「認証ユーザーとその友達が投稿した最新の近況が 20 個返されます」と書かれています (またこの説明には、Twitter の Web サイトから直接 Twitter を使用する人にとっては、「この API を呼び出すことで得られる結果は Twitter の Web サイトで ’/timeline/home’ を指定した場合に得られる結果と等価である」ことも書かれています)。friends_timeline はすべての Twitter API に共通する非常に基本的な要件なので、これを Scitter クラスに追加しましょう。ここではオブジェクトではなくクラスに追加しますが、その理由はドキュメントにあるとおり、friends_timeline が認証ユーザーのためのものだからです (前回の記事で、認証ユーザーの処理は Scitter オブジェクトに属すのではなく、Scitter クラスに属すということを決めました)。

しかし、friends_timelineScitter クラスに追加するには少し面倒な部分があります。friends_timeline という呼び出しは、返される結果を制御する一連の「オプション・パラメーター」(since_idmax_idcountpage) を受け付けます。ところが、Scala は他の言語 (Groovy や JRuby、JavaScript など) とは異なり、ネイティブでは「オプション・パラメーター」の概念をサポートしていません。このため、巧妙な処理が必要になるはずです。ここでは、まず簡単な処理を作成することにし、通常の、パラメーターを使わない呼び出しを単純に実行する friendsTimeline メソッドを作成しましょう。

リスト 6. 「君の友人を教えてくれれば・・・」 ― friendsTimeline メソッド
package com.tedneward.scitter
{
  class Scitter
  {
    def friendsTimeline : List[Status] =
    {
      val (statusCode, statusBody) =
       Scitter.execute("http://twitter.com/statuses/friends_timeline.xml",
                        username, password)

      if (statusCode == 200)
      {
        val responseXML = XML.loadString(statusBody)

        val statusListBuffer = new ListBuffer[Status]

        for (n <- (responseXML \\ "status").elements)
          statusListBuffer += (Status.fromXml(n))
        
        statusListBuffer.toList
      }
      else
      {
        Nil
      }
    }
  }
}

ここまでは特に問題ありません。これをテストするためのメソッドは次のようなものです。

リスト 7. 「・・・君がどういう人間か言ってみせよう」(ミゲル・デ・セルバンテス) ― Scitter.friendsTimeline のテスト
package com.tedneward.scitter.test
{
  class ScitterTests
  {
    // ...
	
    @Test def scitterFriendsTimeline =
    {
      val scitter = new Scitter(testUser, testPassword)
      val result = scitter.friendsTimeline
      assertTrue(result.length > 0)
    }
  }
}

完璧です。これは Scitter オブジェクトからは publicTimeline() メソッドとまったく同じように見え、また動作もほとんど同じです。

しかしこれでもまだ、オプション・パラメーターの問題が残っています。Scala には言語の機能としてオプション・パラメーターがないため、最初の段階での唯一の選択肢は、friendsTimeline() メソッドのオーバーロードを大量に作成し、階乗的な数になるパラメーターを引数に取ることのように思えます。

幸いなことに、もっと良い方法があります。それは Scala 言語の 2 つの機能を興味深い方法で組み合わせる方法です (この 2 つの機能のうち、1 つについてはまだ触れていません)。つまりcase クラスと「繰り返しパラメーター (Repeated Parameter)」とを組み合わせるのです (リスト 8)。

リスト 8. 「どれほどあなたを愛しているか・・・」 ― オプション・パラメーターに対応した friendsTimeline() メソッド
package com.tedneward.scitter
{
  // ...
  
  abstract class OptionalParam
  case class Id(id : String) extends OptionalParam
  case class UserId(id : Long) extends OptionalParam
  case class Since(since_id : Long) extends OptionalParam
  case class Max(max_id : Long) extends OptionalParam
  case class Count(count : Int) extends OptionalParam
  case class Page(page : Int) extends OptionalParam
  
  class Scitter(username : String, password : String)
  {
    // ...
	
    def friendsTimeline(options : OptionalParam*) : List[Status] =
    {
      val optionsStr =
        new StringBuffer("http://twitter.com/statuses/friends_timeline.xml?")
      for (option <- options)
      {
        option match
        {
          case Since(since_id) =>
            optionsStr.append("since_id=" + since_id.toString() + "&")
          case Max(max_id) =>
            optionsStr.append("max_id=" + max_id.toString() + "&")
          case Count(count) =>
            optionsStr.append("count=" + count.toString() + "&")
          case Page(page) =>
            optionsStr.append("page=" + page.toString() + "&")
        }
      }
      
      val (statusCode, statusBody) =
        Scitter.execute(optionsStr.toString(), username, password)
      if (statusCode == 200)
      {
        val responseXML = XML.loadString(statusBody)

        val statusListBuffer = new ListBuffer[Status]

        for (n <- (responseXML \\ "status").elements)
          statusListBuffer += (Status.fromXml(n))
        
        statusListBuffer.toList
      }
      else
      {
        Nil
      }
    }
  }
}

options パラメーターの最後に * タグが付いていることに気づきましたか?このタグは、このパラメーターが実際には複数のパラメーターのシーケンスであることを示しており、Java 5 の varargs 構成体と非常によく似ています。varargs の場合と同じように、渡すパラメーターの数を先ほどの場合と同じようにゼロにすることもできます (ただしここでテスト・コードに戻り、friendsTimeline の呼び出しに 1 対の括弧を追加する必要があります。そうしないとコンパイラーは、パラメーターなしでメソッドを呼び出そうとしているのか、それともアプリケーションを呼び出す目的やそれと同様の目的で friendsTimeline を使おうとしているのかを判断することができません)。あるいは、下記のようにテスト・コードで friendsTimeline に case クラスを渡すこともできます。

リスト 9. 「・・・私の気持ちを伝えさせてください」(ウィリアム・シェークスピア) ― オプション・パラメーターに対応した Scitter.friendsTimeline のテスト
package com.tedneward.scitter.test
{
  class ScitterTests
  {
    // ...
	
    @Test def scitterFriendsTimelineWithCount =
    {
      val scitter = new Scitter(testUser, testPassword)
      val result = scitter.friendsTimeline(Count(5))
      assertTrue(result.length == 5)
    }
  }
}

もちろん、反社会的なクライアントが極めて異様なパラメーター・シーケンス (例えば friendsTimeline(Count(5), Count(6), Count(7)) など) を渡す可能性は常にあります。しかし、ここでは単純にパラメーターのリストをそのまま Twitter に渡します (そして Twitter のエラー処理が強力であり、指定のパラメーターのうち最後のパラメーターのみを取るものと祈ることにします)。異様なパラメーター・シーケンスを渡される懸念が出てきた場合に、繰り返しパラメーターのリストをウォークスルーし、それぞれの種類のパラメーターのうち最後のパラメーターを使って Twitter への送信用の URL を構築することは難しくはありません。差し当たりここでは、パラメーター・シーケンスの処理は Twitter に任せることにします。

互換性

ここまでのところで、興味深い疑問が湧いてきます。この friendsTimeline() メソッドを Java コードから呼び出すことは、どの程度容易なのでしょう。つまり、このライブラリーの大きな目標の 1 つが Java コードとの互換性を保つことであれば、このライブラリーを Java コードで使うことが困難ではないようにする必要があります。

そこでまず、おなじみの javap を Scitter クラスに対して実行してみましょう。

リスト 10. Java コードを思い出してみましょう ― Scitter クラスに javap を実行する
C:\>javap -classpath classes com.tedneward.scitter.Scitter
Compiled from "scitter.scala"
public class com.tedneward.scitter.Scitter extends java.lang.Object implements s
cala.ScalaObject{
    public com.tedneward.scitter.Scitter(java.lang.String, java.lang.String);
    public scala.List friendsTimeline(scala.Seq);
    public boolean verifyCredentials();
    public int $tag()       throws java.rmi.RemoteException;
}

思ったとおり、2 つの懸念事項が見つかりました。第 1 に、friendsTimeline()scala.Seq をパラメーターとして使っています (たった今、この繰り返しパラメーターの機能を使用しました)。第 2 に、friendsTimeline() メソッドは Scitter オブジェクトの publicTimeline() メソッドの場合とまったく同様に (信じられない場合には皆さん自身が Scitter オブジェクトの publicTimeline() メソッドに対して javap を実行してダブルチェックしてみてください)、要素の scala.List を返します。この 2 つのタイプのメソッドを Java コードから使う場合、使いやすさはどの程度なのでしょう。

それを知る最も簡単な方法は、Scala ではなく Java コードを使って簡単な一連の JUnit テストを作成してみることです。そこで、実際にそうしてみましょう。Scitter インスタンスの構造をテストしたり、Scitter インスタンスの verifyCredentials() メソッドを呼び出したりすることもできますが、それはあまり有益ではありません。ここでの (この場合の) 目的は Scitter クラスが適切かどうかを検証することではなく、Java コードから Scitter インスタンスを使うことがどの程度容易であるかを調べることであるということを忘れないでください。そこで、「友達のタイムライン」を取得するためのテストの作成に移りましょう。つまり、Scitter インスタンスをインスタンス化し、このインスタンスの friendsTimeline() メソッドを、パラメーターなしで呼び出す必要があります。

そうしようとすると、scala.Seq パラメーターを渡す必要があるため、少し面倒です。scala.Seq は Scala の trait です。つまり scala.Seq はベースの JVM にインターフェースとしてマッピングされ、scala.Seq を直接インスタンス化することはできません。Java の典型的なパラメーター null を使うこともできますが、その場合は実行時に例外がスローされてしまいます。ここでは Java コードから容易にインスタンス化できる scala.Seq クラスが必要なのです。

調べてみると、Scitter の実装自体の内部で使用している、まさに mutable.ListBuffer の中に、そうしたクラスがあることがわかります。

リスト 11. これを見ると、なぜ私が Scala を気に入っているのか思い出すことができます ― mutable.ListBuffer を使用する
package com.tedneward.scitter.test;

import org.junit.*;
import com.tedneward.scitter.*;

public class JavaScitterTests
{
  public static final String testUser = "TESTUSER";
  public static final String testPassword = "TESTPASSWORD";
  
  @Test public void getFriendsStatuses()
  {
    Scitter scitter = new Scitter(testUser, testPassword);
    if (scitter.verifyCredentials())
    {
      scala.List statuses =
        scitter.friendsTimeline(new scala.collection.mutable.ListBuffer());
      Assert.assertTrue(statuses.length() > 0);
    }
    else
      Assert.assertTrue(false);
  }
}

返される scala.List を使うことは問題ではありません。scala.List の扱い方は他の任意の Collection クラスの扱い方とまったく同じです (ただし List に対する Scala ベースのメソッドは Scala からやり取りを行うことを前提としているため、Collections API の利点は一部失われます)。そのため、結果をウォークスルーすることは (1995年頃の少しばかり古めかしい Java コードを我慢すれば) それほど難しくありません。

リスト 12. ベクトルを 1995年に戻す ― friendsTimeline() メソッドの実行結果をウォークスルーする
package com.tedneward.scitter.test;

import org.junit.*;
import com.tedneward.scitter.*;

public class JavaScitterTests
{
  public static final String testUser = "TESTUSER";
  public static final String testPassword = "TESTPASSWORD";

  @Test public void getFriendsStatuses()
  {
    Scitter scitter = new Scitter(testUser, testPassword);
    if (scitter.verifyCredentials())
    {
      scala.List statuses =
        scitter.friendsTimeline(new scala.collection.mutable.ListBuffer());
      Assert.assertTrue(statuses.length() > 0);
      
      for (int i=0; i<statuses.length(); i++)
      {
        Status stat = (Status)statuses.apply(i);
        System.out.println(stat.user().screenName() + " said " + stat.text());
      }
    }
    else
      Assert.assertTrue(false);
  }
}

今度は次の部分、つまり friendsTimeline() メソッドにパラメーターを渡す部分です。残念ながら、ListBuffer はコンストラクターのパラメーターとしてコレクションを取りません。そこでパラメーターのリストを作成し、メソッドが呼び出されたらコレクションを渡すようにする必要があります。この作業は退屈ですがそれほど大変なものではありません。

リスト 13. そろそろ Scala に戻してもよいですか? ― friendsTimeline() メソッドにコレクションを渡すようにする
package com.tedneward.scitter.test;

import org.junit.*;
import com.tedneward.scitter.*;

public class JavaScitterTests
{
  public static final String testUser = "TESTUSER";
  public static final String testPassword = "TESTPASSWORD";
  
  // ...

  @Test public void getFriendsStatusesWithCount()
  {
    Scitter scitter = new Scitter(testUser, testPassword);
    if (scitter.verifyCredentials())
    {
      scala.collection.mutable.ListBuffer params =
        new scala.collection.mutable.ListBuffer();
      params.$plus$eq(new Count(5));
      
      scala.List statuses = scitter.friendsTimeline(params);

      Assert.assertTrue(statuses.length() > 0);
      Assert.assertTrue(statuses.length() == 5);
      
      for (int i=0; i<statuses.length(); i++)
      {
        Status stat = (Status)statuses.apply(i);
        System.out.println(stat.user().screenName() + " said " + stat.text());
      }
    }
    else
      Assert.assertTrue(false);
  }
}

これを見るとわかるように、Java バージョンは対応する Scala バージョンよりも少し冗長ですが、それでも Java クライアントから非常に容易にこの Scitter ライブラリーを呼び出すことができます。素晴らしい結果です。

まとめ

明らかなことですが、Scitter にはまだ多分に改善の余地があります。しかし本格的な形ができつつあり、ここまでは快調です。この記事では Scitter ライブラリーの通信部分に DRY 原則を適用し、また Twitter に用意されたいくつもの異なる API を呼び出す際に必要な、オプション・パラメーターに対応しました。そしてこれまでのところ、この記事で公開した API は Java クライアントにとってそれほど使いにくいものではありません。確かに、この API は Scala で自然に利用できる API ほどにはすっきりとしていませんが、Java 開発者が Scitter ライブラリーを使おうとする場合にも大がかりな準備をする必要はありません。

この Scitter ライブラリーはまだ相変わらず「オブジェクト指向風」の面影を残していますが、Scala による「関数型風」の特徴が見え始めています。このライブラリーの構築を続けていけば、「関数型風」のさまざまな特徴が徐々に引き出されていき、コードがより簡潔かつ明瞭になっていくことでしょう。そして、まさにそうあるべきなのです。

さて、これで皆さんとお別れし、私が少し休みを取る時間になりました。次回は、オフライン・テストのサポートを追加し、またこのライブラリーにユーザーの近況を更新する機能を追加します。それまでの間、Scala ファンの皆さん、使い物にならない (dysfunctional) 言語よりも使い物になる (functional) 関数型 (functional) の言語の方が常に優れていることを忘れないでください (どうも私はこの冗談を言いすぎるようで申し訳ありません)。


ダウンロード可能なリソース


関連トピック

  • Scala と XML」(Michael Galpin 著、developerWorks、2008年4月) は Scala によって XML を楽しく扱えるようになることを解説しています。
  • Scala の lift フレームワークは Web アプリケーションを作成するためのフレームワークです。このウィキでは、lift の基礎を支える有名なフレームワーク (Seaside、Rails、Django、Wicket など) を解説しています。入門用のチュートリアルはここにあります。
  • Functional programming in the Java language」(Abhijit Belapurkar 著、developerWorks、2004年7月) は、Java 開発者の視点から関数型プログラミングの利点と使い方を説明しています。
  • Scala by Example」(Martin Odersky 著、2007年12月) は簡潔に、コードを中心に Scala を紹介しています。
  • Programming in Scala』(Martin Odersky、Lex Spoon、Bill Venners の共著、2007年12月、Artima 刊) は 1 冊の本として Scala を紹介した最初の資料です。
  • C++ を設計し、実装した Bjarne Stroustrup は、C++ を「より優れた C」と表現しています。
  • Java Puzzlers 罠、落とし穴、コーナーケース』 (2005年7月ピアソン・エデュケーション刊) は Java プログラミング言語の風変わりな点を、楽しいながらも深く考えさせるプログラミング・パズルをとおして明らかにしています。
  • developerWorks の Java technology ゾーンには、Java プログラミングのあらゆる側面を網羅した記事が豊富に用意されています。
  • Jakarta (Apache) Commons の HttpClient コンポーネントには、最新の HTTP 標準と推奨事項のクライアント・サイドを実装した、効率的で最新の豊富な機能を備えたパッケージが提供されています。また Commons Logging コンポーネントと Commons Codec コンポーネントも必要です。
  • Scala をダウンロードし、このシリーズと共に Scala を学んでください。
  • SUnit は Scala の標準的なディストリビューションの一部であり、scala.testing パッケージの中にあります。

コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology, Web development, Open source
ArticleID=405359
ArticleTitle=多忙な Java 開発者のための Scala ガイド: Scitter ライブラリーを機能強化する
publish-date=06022009