多忙な Java 開発者のための Scala ガイド: Scala + Twitter = Scitter

より「ソーシャル」な方法によるネットワーキングを Scala によって実現する

Scala の概要について説明することは楽しいのですが、このシリーズの読者の大部分にとっては Scala を実用的な方法で使ってみないと、Scala を開発者の「おもちゃ」として捉える場合と、仕事で使う場合の違いはわからないかもしれません。今回の記事では Ted Neward が Scala を使って、人気のマイクロブロギング・システムである Twitter にアクセスするためのクライアント・ライブラリーの基本フレームワークを作成します。

Ted Neward, Principal, ThoughtWorks

Ted Neward photoTed Neward は世界規模でコンサルティングを行う ThoughtWorks のコンサルタントであり、また Neward & Associates の代表として、Java や .NET、XML サービスなどのプラットフォームに関するコンサルティング、助言、指導、講演を行っています。彼はワシントン州シアトルの近郊に在住しています。



2009年 5月 05日

Twitter はインターネットに嵐を引き起こしています。おそらく皆さんもご存じと思いますが、この「気の利いた」ソーシャル・ネットワーキング・ツールのユーザーは、自分自身のことや自分が今何をしているかについて、簡単な近況のアップデートを提供することができます。フォロワーは Twitter フィードでアップデートを受信しますが、これはブログによって読み手のフィードにアップデートが生成されるのとほとんど同じです。

このシリーズについて

このシリーズでは、Ted Neward が皆さんと共に Scala プログラミング言語を深く掘り下げ、Scala が最近もてはやされている理由を調べ、Scala の言語機能の実際の動作を調べます。Scala のコードと Java™ のコードの比較が重要な場合には両者のコードを並べて示しますが、(これから学ぶように) Scala の機能のうちの多くは、Java には直接対応するものがありません。そして Scala の魅力の多くがあるのはそこなのです。結局のところ、Java で可能ならば、手間をかけて Scala を学ぶ必要はないのです。

Twitter そのものは、興味深い会話が交わされるソーシャル・ネットワーキングであり、「常にネットワークに接続している」新しいユーザー世代によって利用されています。Twitter に関しては、ありとあらゆる賛否両論が入り乱れています。

Twitter は初期の頃から API を公開していたため、インターネット上には Twitter 用のクライアント・アプリケーションが無数にあります。Twitter の API のベースは基本的にとても単純で理解しやすいため、自分で Twitter クライアントを作成すると勉強になることに多くの開発者が気付いています。これは Web 技術を学ぶ開発者が学習用の演習として独自のブログ・サーバーを作成することとよく似ています。

Scala が持つ関数型の性質は Twitter の RESTful な性質との親和性が高いように思われますが、この関数型の性質と非常に優れた XML 処理機能を考えると、Twitter にアクセスするための Scala クライアント・ライブラリーを作成することが Twitter に Scala を利用する実験としては適切なようです。

Twitter とは何か

この話題に深く入る前に、まだ Twitter の API を見たことのない人 (あるいはその技術を使ったことがない人) のために、まずこの API を調べてみましょう。

一言で言えば、Twitter は「マイクロブログ」です。Twitter はパーソナライズされたフィードであり、ユーザー自身に関する簡単な記述を含み、長さは 140 文字を超えることがなく、「フォローしたい」と望む人は誰でも、Web の更新や RSS、テキスト・メッセージなど利用して、そのフィードを受信することができます。(140 文字という制限が必要な理由は、Twitter の基本ソース・チャネルの 1 つであるテキスト・メッセージが 140 文字に制限されているためにすぎません)。

「ほとんど RESTful」の意味

読者のなかには、なぜ私が「ほとんど RESTful」という表現を使うのか、興味があるかもしれません。「Twitter の API は REST (Representational State Transfer) の設計原則に準拠しようとしています。」そして大まかに言えば、Twitter の API は実際に REST に準拠しています。REST の概念を最初に提唱した Roy Fielding であれば、Twitter に REST という言葉を使うことには同意しないかもしれません。しかし実際には、Twitter の手法は、ほとんどの人達が使っている REST という言葉の意味には適合するのです。正直に告白すると、私は REST とは何かを延々と述べる怒りのコメントを受け取るのは避けたいと思っているにすぎません。そのため私は「ほとんど」という修飾語を使っているのです。

プログラミングの観点から見ると、何らかのメッセージ・フォーマット (XML、ATOM、RSS、JSON など) を使って Twitter サーバーとの間でメッセージを送受信するという意味で、Twitter は「ほとんど RESTful」な API です。さまざまな URL と、さまざまなメッセージ、そしてメッセージの一部 (必須の部分とオプションの部分) とが組み合わされ、さまざまな API 呼び出しが行われます。例えば、すべての「つぶやき」(Twitter のアップデート) の完全なリストを Twitter 上の全員 (公開タイムライン) から受信したい場合には、XML、ATOM、RSS、JSON のいずれかによるメッセージを用意し、それを適切な URL に送信し、そしてその結果を Twitter の Web サイト (apiwiki.twitter.com) に説明されたフォーマットで利用します。このサイトには以下の説明があります。

------------------------------------------------------------
public_timeline (公開タイムライン)
カスタム・ユーザー・アイコンを設定済みで、つぶやきを公開しているユーザー
からの最新の近況を 20 個返します。認証は必要ありません。

公開タイムラインは 60 秒間キャッシュされることに注意してください。そのため、
キャッシュ期間よりも短い間隔で公開タイムラインを要求することはリソースの浪費です。

URL:http://twitter.com/statuses/public_timeline.format
フォーマット: XML、JSON、RSS、ATOM
メソッド: GET
API 制限: なし
戻り値: 近況要素の一覧
------------------------------------------------------------

これをプログラミングの観点から見ると、単純な HTTPGET リクエストを Twitter サーバーに送信すると、XML、RSS、ATOM、JSON いずれかのメッセージにラップされた「近況」メッセージの一覧が返されるということです。Twitter のサイトでは、近況メッセージはおおよそリスト 1 のようなものとして定義されています。

リスト 1. 近況メッセージ
<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom">
  <title>Twitter / tedneward</title>
  <id>tag:twitter.com,2007:Status</id>
  <link type="text/html" rel="alternate" href="http://twitter.com/tedneward"/>
  <updated>2009-03-07T13:48:31+00:00</updated>
  <subtitle>Twitter updates from Ted Neward / tedneward.</subtitle>
  <entry>
	<title>tedneward: @kdellison Happens to the best of us...</title>
	<content type="html">tedneward: @kdellison Happens to the best of us...</content>
	<id>tag:twitter.com,2007:http://twitter.com/tedneward/statuses/1292396349</id>
	<published>2009-03-07T11:07:18+00:00</published>
	<updated>2009-03-07T11:07:18+00:00</updated>
	<link type="text/html" rel="alternate"
        href="http://twitter.com/tedneward/statuses/1292396349"/>
	<link type="image/png" rel="image"
        href="http://s3.amazonaws.com/twitter_production/profile_images/
         55857457/javapolis_normal.png"/>
	<author>
	  <name>Ted Neward</name>
	  <uri>http://www.tedneward.com</uri>
	</author>
  </entry>
</feed>

近況メッセージの (すべてとは言わないまでも) 大部分の要素は非常に単純なので、これらの要素の説明は省略し、皆さんの想像におまかせすることにします。

Twitter のメッセージは、XML ベースの 3 つのフォーマットの 1 つを利用することができます。そして、Scala には (XML リテラルや XPath 風のクエリー構文による API など) 強力な XML 機能がいくつかあります。このため、Twitter メッセージを送受信する Scala ライブラリーを作成すると、Scala の基本的なコーディングの演習になります。例えば、Scala を使ってリスト 1 のメッセージから近況のアップデートのタイトルまたは内容を抽出するためには、Scala の XML 型と \ メソッドと \\ メソッドを使います (リスト 2)。

リスト 2. 近況メッセージからのタイトルの抽出
<![CDATA[
package com.tedneward.scitter.test
{
  class ScitterTest
  {
    import org.junit._, Assert._
    
    @Test def simpleAtomParse =
    {
      val atom =
        <feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom">
          <title>Twitter / tedneward</title>
          <id>tag:twitter.com,2007:Status</id>
          <link type="text/html" rel="alternate" href="http://twitter.com/tedneward"/>
          <updated>2009-03-07T13:48:31+00:00</updated>
          <subtitle>Twitter updates from Ted Neward / tedneward.</subtitle>
          <entry>
            <title>tedneward: @kdellison Happens to the best of us...</title>
            <content type="html">tedneward: @kdellison
                                 Happens to the best of us...</content>
            <id>tag:twitter.com,2007:
                http://twitter.com/tedneward/statuses/1292396349</id>
            <published>2009-03-07T11:07:18+00:00</published>
            <updated>2009-03-07T11:07:18+00:00</updated>
            <link type="text/html" rel="alternate"
                  href="http://twitter.com/tedneward/statuses/1292396349"/>
            <link type="image/png" rel="image"
                  href="http://s3.amazonaws.com/twitter_production/profile_images/
                        55857457/javapolis_normal.png"/>
            <author>
              <name>Ted Neward</name>
              <uri>http://www.tedneward.com</uri>
            </author>
          </entry>
        </feed>
    
      assertEquals(atom \\ "entry" \ "title",
		 "tedneward: @kdellison Happens to the best of us...")
    }
  }
}
]]>

Scala による XML サポートの詳細については「Scala と XML」を読んでください (「参考文献」を参照)。

演習としては、そのままの XML を処理するだけでは楽しくありません。Scala は私達が楽をするように設計されているので、Scala を使って楽をすることにしましょう。そこで、Twitter メッセージの送受信を容易にするための専用のクラスやクラスのコレクションを作成します。そのためには、この Scala ライブラリーが「通常の」Java プログラムから容易に使用できる必要があります (つまり、通常の Java のセマンティクスを理解する任意のもの (Groovy または Clojure など) から容易にアクセスできる必要があるということです)。

API の設計

Scala/Twitter ライブラリー (ThoughtWorker での私の同僚である Neal Ford の提案に従って、このライブラリーを「Scitter」と呼ぶことにします) の API の設計の詳細に入る前に、いくつかの要件について詳しく検討する必要があります。

第 1 に、当然のことですが、Scitter は何らかの形でネットワークにアクセスする必要があり、さらには Twitter サーバーにアクセスする必要がありますが、これによってテストは面倒になります。

第 2 に、Twitter から返されるさまざまなフォーマットを解析 (そしてテスト) する必要があります。

第 3 に、この API の背後にあるさまざまなフォーマット間の違いを隠す必要があります。そうすればクライアントは Twitter のドキュメントに記されたメッセージ・フォーマットを気にする必要がなくなり、標準的なクラスを扱う場合と同じ処理をするだけでよくなります。

そして最後に、Twitter の「認証ユーザー」でなければ大量にある Twitter の API を利用できないため、Scitter ライブラリーは「認証」API と「非認証」API との違いに適切に対応する必要があります。ただし、そのために過度に複雑になることは避けなければなりません。

ネットワークにアクセスして Twitter サーバーに接続するためには、何らかの形の HTTP 通信が必要です。Twitter の Java ライブラリー (具体的には URL クラスやその仲間) を使うこともできますが、Twitter の API ではリクエスト本体とレスポンス本体を使った接続を大量に必要とするため、別の HTTP API、具体的には Apache Commons の HttpClient ライブラリーを使った方が容易だということがわかります。クライアント API のテストを容易にするために、実際の通信は Scitter ライブラリーの内部で処理され、API の背後に隠されます。そうすることで、HttpClient ライブラリーを別の HTTP ライブラリーと交換することが容易になるだけではなく (とは言っても交換の必要性は考えられません)、実際のネットワーク通信のモックも簡単に作成できるようになるため、テストも楽になります (この必要性は容易に想像できます)。

そのため、最初のテストとしては、HttpClient への呼び出しを単純に Scala 化し、基本的な通信パターンが用意できていることを確認することです。注意点として、HttpClient は他の 2 つの Apache ライブラリー (Commons Logging と Commons Codec) に依存するため、この 2 つのライブラリーもテストの実行中に存在している必要があります。皆さんが同様のコードを開発しようとする場合には、この 3 つのライブラリーがすべてクラスパス上に存在していることを確認する必要があります。

最も使いやすい Twitter API はテスト用の API です。この API は以下のように説明されています。

200 OK HTTP ステータス・コードと共に、要求されたフォーマットで「ok」というストリングを返します。

Scitter の実験用のテストの最初の例として、このテスト用の API を取り上げましょう。ドキュメントによれば、この API が提供されている URL は http://twitter.com/help/test.format です (“format” は “xml” または “json” のいずれかで置き換えられますが、ここでは “xml” を使います)。そしてサポートされている HTTP メソッドは GET のみです。従って HttpClient を使ったコードはほとんど自動的に作成できてしまいます (リスト 3)。

リスト 3. Twitter 版の PING
package com.tedneward.scitter.test
{
  class ExplorationTests
  {
    // ...
  
    import org.apache.commons.httpclient._, methods._, params._, cookie._
    
    @Test def callTwitterTest =
    {
      val testURL = "http://twitter.com/help/test.xml"
      
      // HttpClient API 101
      val client = new HttpClient()
      val method = new GetMethod(testURL)

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

      client.executeMethod(method)
      
      val statusLine = method.getStatusLine()
      
      assertEquals(statusLine.getStatusCode(), 200)
      assertEquals(statusLine.getReasonPhrase(), "OK")
    }
  }
}

このコードの大部分は HttpClient を使ったコードにはお決まりのものです。興味のある読者は HttpClient API のドキュメントに詳細が説明されていますので参照してください。テストを実行する際にインターネットに接続できるとすると (そして Twitter が公開 API を変更していないとすると)、このテストは見事に成功するはずです。

このテストを終えたら、Scitter クライアントの最初の部分を詳しく検討しましょう。つまり、どのような設計にするかということです。Scitter クライアントをどのように構成すれば、認証呼び出しと非認証呼び出しとを区別して処理できるのでしょう。ここでは、それを典型的な Scala の方法で行うことにし、認証は「オブジェクトごと」に行われるものとします。従って認証を必要とする呼び出しをクラス定義の中に、認証を必要としない呼び出しをオブジェクト定義の中に入れます。

リスト 4. Scitter.test
package com.tedneward.scitter
{
  /**
   * Object for consuming "non-specific" Twitter feeds, such as the public timeline.
   * Use this to do non-authenticated requests of Twitter feeds.
   */
  object Scitter
  {
    import org.apache.commons.httpclient._, methods._, params._, cookie._

    /**
     * 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
    }
  }
  /**
   * 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)
  {
  }
}

またネットワークの抽象化はそのままにし、後でオフラインのテストが重要になった場合に改善事項として追加することにします。こうすることで、HttpClient クラスの使い方を正確に理解しようとする際にネットワーク通信を過度に抽象化してしまう事態を避けることもできます。

Twitter の認証クライアントと非認証クライアントとを区別できたので、認証メソッドも手早く作成しましょう。Twitter には、ユーザーがログインするためのクレデンシャルを検証する API があります。これは、これから作成しようとしている認証動作付きの ping に非常に近いものです。この場合も HttpClient を使ったコードは先ほどのコードと似ていますが、今度はユーザー名とパスワードも Twitter API に渡す必要がある点が異なります。

これによって、Twitter でのユーザー認証の考え方がわかります。Twitter API のページを簡単に調べてみると、Twitter は保存型の HTTP 認証の手法を使っていることがわかります。これは認証されたリソースが HTTP の中で行う方法と同じです。つまり HttpClient を使ったコードはユーザー名とパスワードを HTTP リクエストの中で提供する必要がありますが、ご想像とは異なり、POST される本体として提供するわけではありません (リスト 5)。

リスト 5. Twitter にクレデンシャルを渡す
package com.tedneward.scitter.test
{
  class ExplorationTests
  {
    def testUser = "TwitterUser"
	def testPassword = "TwitterPassword"
  
    @Test def verifyCreds =
    {
      val client = new HttpClient()

      val verifyCredsURL = "http://twitter.com/account/verify_credentials.xml"
      val method = new GetMethod(verifyCredsURL)

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

      client.getParams().setAuthenticationPreemptive(true)
      val defaultcreds = new UsernamePasswordCredentials(testUser, testPassword)
      client.getState().setCredentials(new AuthScope("twitter.com", 80,
                                                     AuthScope.ANY_REALM), defaultcreds)
      
      client.executeMethod(method)
      
      val statusLine = method.getStatusLine()
      
      assertEquals(200, statusLine.getStatusCode())
      assertEquals("OK", statusLine.getReasonPhrase())
    }
  }
}

このテストにパスするためには、ユーザー名のフィールドとパスワードのフィールドに Twitter が受け付けるものを入力する必要があることに注目してください。私は私自身の Twitter ユーザー名とパスワードを使ってテストを行いましたが、当然ながら皆さんは皆さん自身のユーザー名とパスワードを使う必要があります。Twitter への新しいアカウントの登録は非常に簡単なので、皆さんは既にユーザー名とパスワードを持っているか、または持っていなくてもその取得方法を理解できるものとします (それまで私は待つことにします)。

フィールドに入力したユーザー名とパスワードが Twitter に受け付けられるようになったら、同様のことを Scitter クラス自体の中で行えばよいということが容易にわかります。この場合にはユーザー名とパスワードのコンストラクター・パラメーターを使います (リスト 6)。

リスト 6. Scitter.verifyCredentials
package com.tedneward.scitter
{
  import org.apache.commons.httpclient._, auth._, methods._, params._

  // ...

  /**
   * 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
    }
  }
}

これに対応する、Scitter クラスのテストも非常に簡単です (リスト 7)。

リスト 7. Scitter.verifyCredentials のテスト
package com.tedneward.scitter.test
{
  class ScitterTests
  {
    import org.junit._, Assert._
    import com.tedneward.scitter._

    def testUser = "TwitterUsername"
      def testPassword = "TwitterPassword"
    
      // ...
	
    @Test def verifyCreds =
    {
      val scitter = new Scitter(testUser, testPassword)
      val result = scitter.verifyCredentials
      assertTrue(result)
    }
  }
}

結果は悪くありません。このライブラリーの基本的な構造が形成されつつありますが、まだ完成が遠いことは明らかです。何よりも、まだ Scala 特有のことを何もしていません。ライブラリーの作成は単にオブジェクト指向設計の演習にすぎません。そこで、実際に XML を使用し、使いやすい形で XML を返す方法の検討を始めましょう。

XML からオブジェクトへ

現在の状態で最も容易に追加できる API は public_timeline API です。この API は、Twitter がすべてのユーザーから受信した最新の n 個のアップデートを収集し、それを使用できるように返します。これまでに見た 2 つの API とは異なり、public_timeline API は (ステータス・コードのみに依存するのではなく) レスポンス本体を返します。そのため、このレスポンス本体の XML/RSS/ATOM/その他を分割してから Scitter クライアントに返す必要があります。

ここまでの手順と同様に、実験用のテストを作成しましょう。このテストは公開フィードの URL にアクセスし、その結果を検証用に stdout に出力します (リスト 8)。

リスト 8. 公開フィードにアクセスする
package com.tedneward.scitter.test
{
  class ExplorationTests
  {
    // ...
  
    @Test def callTwitterPublicTimeline =
    {
      val publicFeedURL = "http://twitter.com/statuses/public_timeline.xml"
      
      // HttpClient API 101
      val client = new HttpClient()
      val method = new GetMethod(publicFeedURL)
      method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, 
        new DefaultHttpMethodRetryHandler(3, false))
      
      client.executeMethod(method)
      
      val statusLine = method.getStatusLine()
      assertEquals(statusLine.getStatusCode(), 200)
      assertEquals(statusLine.getReasonPhrase(), "OK")
      
      val responseBody = method.getResponseBodyAsString()
      System.out.println("callTwitterPublicTimeline got... ")
      System.out.println(responseBody)
    }
  }
}

これを実行すると、結果は毎回異なります。これは Twitter の公開サーバー上に膨大な数のユーザーがいるためですが、その結果はおおよそ、JUnit テキスト・ファイルをダンプしたリスト 9 のようになります。

リスト 9. 公開フィードにアクセスして収集したつぶやき
<statuses type="array">
    <status>
      <created_at>Tue Mar 10 03:14:54 +0000 2009</created_at>
      <id>1303777336</id>
      <text>She really is. http://tinyurl.com/d65hmj</text>
      <source><a href="http://iconfactory.com/software/twitterrific">twitterrific</a>
        </source>
      <truncated>false</truncated>
      <in_reply_to_status_id></in_reply_to_status_id>
      <in_reply_to_user_id></in_reply_to_user_id>
      <favorited>false</favorited>
      <user>
          <id>18729101</id>
          <name>Brittanie</name>
          <screen_name>brittaniemarie</screen_name>
          <description>I'm a bright character. I suppose.</description>
          <location>Atlanta or Philly.</location>
          <profile_image_url>http://s3.amazonaws.com/twitter_production/profile_images/
                             81636505/goodish_normal.jpg</profile_image_url>
          <url>http://writeitdowntakeapicture.blogspot.com</url>
          <protected>false</protected>
          <followers_count>61</followers_count>
      </user>
    </status>
    <status>
      <created_at>Tue Mar 10 03:14:57 +0000 2009</created_at>
      <id>1303777334</id>
      <text>Number 2 of my four life principles.  "Life is fun and rewarding"</text>
      <source>web</source>
      <truncated>false</truncated>
      <in_reply_to_status_id></in_reply_to_status_id>
      <in_reply_to_user_id></in_reply_to_user_id>
      <favorited>false</favorited>
      <user>
          <id>21465465</id>
          <name>Dale Greenwood</name>
          <screen_name>Greeendale</screen_name>
          <description>Vegetarian. Eat and use only organics. 
                       Love helping people become prosperous</description>
          <location>Melbourne Australia</location>
          <profile_image_url>http://s3.amazonaws.com/twitter_production/profile_images/
                             90659576/Dock_normal.jpg</profile_image_url>
          <url>http://www.4abundance.mionegroup.com</url>
          <protected>false</protected>
          <followers_count>15</followers_count>
       </user>
     </status>
       (A lot more have been snipped)
</statuses>

この結果と Twitter のドキュメントの両方から判断すると、この public_timeline API を呼び出した結果はほとんど単純に「近況」メッセージを集めたものであり、メッセージは一貫した構造を持っている、ということが明らかです。Scala は XML をサポートしているため、この結果を分割するのは非常に簡単です。ただし基本的なテストにパスしたら、即座にこのテストを改善します (リスト 10)。

リスト 10. 公開フィードを構文解析する
package com.tedneward.scitter.test
{
  class ExplorationTests
  {
    // ...
  
    @Test def simplePublicFeedPullAndParse =
    {
      val publicFeedURL = "http://twitter.com/statuses/public_timeline.xml"
      
      // HttpClient API 101
      val client = new HttpClient()
      val method = new GetMethod(publicFeedURL)
      method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, 
        new DefaultHttpMethodRetryHandler(3, false))
      val statusCode = client.executeMethod(method)
      val responseBody = new String(method.getResponseBody())
      
      val responseXML = scala.xml.XML.loadString(responseBody)
      val statuses = responseXML \\ "status"

      for (n <- statuses.elements)
      {
        n match
        {
          case <status>{ contents @ _*}</status> =>
          {
            System.out.println("Status: ")
            contents.foreach((c) =>
              c match
              {
                case <text>{ t @ _*}</text> =>
                  System.out.println("\tText: " + t.text.trim)
                case <user>{ contents2 @ _* }</user> =>
                {
                  contents2.foreach((c2) =>
                    c2 match
                    {
                      case <screen_name>{ u }</screen_name> =>
                        System.out.println("\tUser: " + u.text.trim)
                      case _ =>
						()
                    }
                  )
                }
                case _ =>
				  ()
              }
            )
          }
          case _ =>
            () // or, if you prefer, System.out.println("Unrecognized element!")
        }
      }
    }
  }
}

サンプル・コードのパターンを見ると、あまり心強いとは言えません。これはむしろ DOM のようであり、1 度に 1 つの子要素にナビゲートしてテキストを抽出し、次に別のノードまでナビゲートしています。こうするよりも、単純に XPath スタイルの 2 つのクエリーを実行した方が適切です。

リスト 11. 別の構文解析方法
for (n <- statuses.elements)
{
     val text = (n \\ "text").text
       val screenName = (n \\ "user" \ "screen_name").text
}

こちらの方が確かに簡潔ですが、この方法では以下に挙げる基本的な 2 つの問題が発生します。

  • 取得した公開フィードの中で、目的とする各要素あるいはサブ要素を Scala の XML ライブラリーがひと通りトラバースすることになるかもしれず、処理が遅くなる可能性があります。
  • 相変わらず XML メッセージの構造を直接処理しています。2 つの問題のうち、こちらの方がはるかに重要です。

言ってみれば、これはスケーラブルではありません。最終的には Twitter の近況メッセージの中にあるすべての要素を処理することを考えると、各要素をそれぞれの近況メッセージから個別に抽出する必要があります。

すると、個々のフォーマット自体をどうするかという別の問題が出てきます。Twitter が 4 種類のフォーマットをサポートしており、それらの違いを Scitter クライアントがまったく意識せずにすむようにしたい、ということを思い出してください。このために、Scitter には中間的な構造が必要です。リスト 12 のコードのような中間的な構造をクライアントに返し、それをクライアントが利用するようにします。

リスト 12. 近況メッセージ (Status) を分割する
abstract class Status
{
  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
}

そして、User 用にもこの Status と同様のものが必要ですが、繰り返しになるので、ここでは省略します。User の子要素には興味深い注意点があります。Twitter の公開タイムラインに含まれるユーザー要素には、「最新の近況メッセージ」を内部にネストするオプションがあります。一方で近況メッセージにも、内部にユーザーがネストされています。こうした場合に再帰の問題が起きることを防ぐために、私は User 型を作成することにしました。この User 型は Status 型の内部にネストされ、表示される User データが含まれていますが、Status はネストされていません。こうすることで再帰の問題が起きるのを避けます。(少なくとも、そのはずでしたが、実はこの方法には問題があることがわかりました。)

これで、Twitter メッセージを表すオブジェクト型を作成できたので、XML をデシリアライズするための Scala の一般的なパターンに従って、この型に対応するオブジェクト定義を作成します。このオブジェクト定義には、XML ノードをオブジェクト・インスタンスに分割する fromXml メソッドが含まれています (リスト 13)。

リスト 13. XML を分割する
  /**
   * Object wrapper for transforming (format) into Status instances.
   */
  object Status
  {
    def fromXml(node : scala.xml.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))
      }
    }
  }

この特定のイディオムの優れた点は、Twitter がサポートする他の任意のフォーマットに拡張できることです。つまりノードが保持するコンテンツのタイプが XML、RSS、Atom のどれかを fromXml メソッド自体でチェックしてからノードを分割することもでき、あるいは StatusfromXmlfromRssfromAtomfromJson いずれかのメソッドを持つこともできます。後者の方法では XML ベースのフォーマットと JSON (テキスト・ベースの) フォーマットとを同じように扱えるため、私は後者の方法の方を好んでいます。

好奇心と観察力がある読者は、StatusfromXml メソッドにも、Status にネストされた UserfromXml メソッドにも、先ほど助言したように、ネスト要素をウォークスルーする方法の代わりに XPath スタイルの分解手法が使われていることにお気付きかと思います。この場合には XPath スタイルの手法の方が読みやすいように思えます。ただし幸いなことに、もし後で私の気が変わった場合でも昔ながらのカプセル化のおかげで、Scitter の世界以外の人には誰にも気付かれずに、後でその方法を変更することができます。

Status の中の 2 つのメンバーが Option[T] 型を使用していることに注目してください。Option[T] 型を使用しているのは、これらの要素が Status メッセージから欠落していることがよくあり、またこれらの要素はあったとしても中身が空のことがあるからです (例えば <in_reply_to_user_id></in_reply_to_user_id> など)。まさにこれを表現するために Option[T] が作られています。Option[T] により、この 2 つの要素が空の場合には値として “None” が返されます。(これは Java ベースの互換性から考えると少し利用しにくいのですが、返される Option インスタンスの get() を呼び出せばよいだけであり、それほど手間はかかりません。またこの方法によって、他の方法を使用した場合に起きる「ヌルかゼロ」の問題に適切に対応することができます。)

これで、公開タイムラインを利用するためには、単に以下のようにすればよいことになります。

リスト 14. 公開タイムラインを分割する
    @Test def simplePublicFeedPullAndDeserialize =
    {
      val publicFeedURL = "http://twitter.com/statuses/public_timeline.xml"
      
      // HttpClient API 101
      val client = new HttpClient()
      val method = new GetMethod(publicFeedURL)
      method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, 
        new DefaultHttpMethodRetryHandler(3, false))
      val statusCode = client.executeMethod(method)
      val responseBody = new String(method.getResponseBody())
      
      val responseXML = scala.xml.XML.loadString(responseBody)
      val statuses = responseXML \\ "status"

      for (n <- statuses.elements)
      {
        val s = Status.fromXml(n)
        System.out.println("\t'@" + s.user.screenName + "' wrote " + s.text)
      }
    }

正直に言って、こちらの方がずっと簡潔で使いやすいです。

これらのすべてを Scitter のシングルトンにまとめるためには、単純にクエリーを実行し、個々の Status 要素を解析して抽出し、そしてそれらを連結して List[Status] インスタンスとして返します (リスト 15)。

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

  object Scitter
  {
    // ...
  
    /**
     * 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
      }
    }
  }
}

これが 1 時間の作業でできたことを考えると、結果は悪くありません。もちろん、フル機能の Twitter クライアントにするためには先が長いのですが、この段階で見る限り基本的な動作は適切なようです。

まとめ

Scitter ライブラリーは順調に作成されつつあります。ここまでの段階では、外から見る限り単純ですっきりとしています。これは Scitter のテストの実装が比較的容易なことからもわかり、特に、Scitter API 自体を作成するもととなった実験用のテストと比較すると、それが明らかです。外部のユーザーは Twitter の API やさまざまなフォーマットの複雑な部分を気にする必要がありません。また現状では Scitter ライブラリーのテストは少し面倒ですが (ネットワークに依存することは単体テストの方法として適切ではありません)、この問題はそのうち修正する予定です。

私が Twitter API にオブジェクト指向の感覚を意図的に残したことに注目してください。これは Scala の精神に従っています。つまり、Scala が関数型の機能をいくつかサポートするからと言って、Java の構造がサポートするオブジェクト指向の設計手法をあきらめる必要はありません。関数型の機能に意味がある場合には関数型の機能を採用しますが、「古い方法」が適切と思える場合にはその「古い方法」を生かすのです。

とは言っても、ここで紹介した設計が、問題に取り組む最高の方法であると言うつもりはなく、私の場合このように設計した、ということにすぎません。この記事を書いているのは私であるため、ここでは私の方法を紹介しました。この方法を好まない場合には、皆さん自身が独自のライブラリーを作成して記事を書いてください (そして今後の記事で引用できるように、その URL を私に送ってください)。実際、今後の記事で、これらのすべてをパッケージ化して Scala の「sbaz」パッケージにし、Web 上のどこかに置いて容易にダウンロードできるようにするつもりです。

今回はこれで終わりです。次回は、この Scitter ライブラリーにいくつか興味深い機能を追加し、テストしやすく、また使いやすいものにするための検討を始めます。

参考文献

学ぶために

  • 多忙な Java 開発者のための Scala ガイド」シリーズ (Ted Neward 著、developerWorks) の他の記事も読んでください。
  • Scala と XML」(Michael Galpin 著、developerWorks、2008年4月) は Scala によって XML を楽しく扱えるようになることを解説しています。
  • 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年11月ピアソン・エデュケーション刊) は Java プログラミング言語の風変わりな点を、楽しいながらも深く考えさせるプログラミング・パズルをとおして明らかにしています。
  • developerWorks の Java technology ゾーンには、Java プログラミングのあらゆる側面を網羅した記事が豊富に用意されています。

製品や技術を入手するために

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology, Open source, Architecture
ArticleID=395485
ArticleTitle=多忙な Java 開発者のための Scala ガイド: Scala + Twitter = Scitter
publish-date=05052009