レベル: 中級 Ted Neward, Principal, ThoughtWorks, ThoughtWorks
2009年 6月 02日 Scala の概要について説明することは楽しいのですが、Scala を実用的な方法で使ってみないと、Scala を開発者の「おもちゃ」として捉える場合と、仕事で使う場合の違いはわからないかもしれません。今回は、Twitter にアクセスするための Scala によるクライアント・ライブラリー Scitter を紹介した前回の記事の続きとして、Scala を熱烈に支持する著者の Ted Neward が、前回にも増して便利で興味深い一連の機能を、このクライアント・ライブラリーに追加します。
Scala ファンの皆さん、「多忙な Java 開発者のための Scala ガイド」へようこそ。前回は、ソーシャル・ネットワーキング利用者の間で現在大きな関心を呼んでいるマイクロブロギング・サイト Twitter について説明しました。そして、Twitter の XML/REST ベースの API のおかげで、いかに Twitter が開発者にとって調査や実験の対象として興味深いものになっているかも説明しました。こうした説明をするために、前回は Twitter にアクセスするための Scala ライブラリーである、「Scitter」の基本構造を作り始めました。
 |
このシリーズについて
developerWorks のこのシリーズでは、Ted Neward が皆さんと共に Scala プログラミング言語を深く掘り下げ、Scala が最近もてはやされている理由を調べ、Scala の言語機能の実際の動作を調べます。Scala のコードと Java™ のコードの比較が重要な場合には両者のコードを並べて示しますが、(これから学ぶように) Scala の機能のうちの多くは、Java には直接対応するものがありません。そして Scala の魅力の多くがあるのはそこなのです。結局のところ、Java で可能ならば、手間をかけて Scala を学ぶ必要はないのです。
|
|
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 つの基本コンポーネントに分けることができます。
User と Status という case クラスは、API 呼び出しへの応答として Twitter がクライアントに返送する基本型を表します。これらのクラスには、XML 表現を作成するためのメソッド、および XML 表現から抽出されるメソッドが含まれています。
- Scitter シングルトン・オブジェクトは、認証ユーザーを必要としない操作を処理します。
- Scitter インスタンスは (ユーザー名とパスワードをパラメーターとして)、認証ユーザーを必要とする操作を処理します。
 |
Twitter の API について
Twitter の API をよく理解していない場合には、Twitter API のウィキ・ページを訪れ、少し時間を取ってこの API の詳細を調べてみてください。基本的な事項は単純で、パラメーターは URL クエリーの一部として渡され、レスポンスは 4 つのフォーマット (JSON、XML、ATOM、RSS) のうちのどれか 1 つである、等々です。ただし、どのような API にも言えることですが、細部までよく調べる必要があるため、この記事を読んでいる間はブラウザーで Twitter の API を開いておくと、説明の中で Scala に関する部分に注目しやすくなります。
|
|
この 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())
}
}
}
|
 |
DRY を念頭に置く
『達人プログラマー ― システム開発の職人から名匠への道』をまだ読んだことがない人、または昔読んだきりの人のために説明すると、DRY は「Don't Repeat Yourself」を表します。DRY というのは要するに、皆さんが同じコード片を何度も繰り返し入力しているとすると、それは自分でやったことを繰り返している (Repeat Youreslf) のであり、結局のところ、そうしたまったく同じことを行っているコードは (メソッドのように) 後で修正、機能強化、置き換えが容易にできるように一箇所にまとめたくなるものであるということを示しています。もし『達人プログラマー ― システム開発の職人から名匠への道』を今まで読んだことがなければ、すぐに読む必要がありますので、今月の宿題として読んでください。
|
|
リスト 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 オブジェクトを再利用することでコードを最適化することもできます。
 |
拙速な最適化
ところで、ここで最適化を行っても、HttpClient がコンストラクターの中で何か重要なことを行うのでないと、あまり意味がありません。具体的に言えば、オブジェクトの割り当て時に保存することのみを目的に HttpClient オブジェクトをキャッシュしようとしても、有益な最適化とは言えません。ガーベッジ・コレクターは長年、Java のパフォーマンスに関する文献で大いに批判されていますが、実際にはこの場合での HttpClient オブジェクトの類のような寿命の短いオブジェクトの割り当てや収集については、非常に適切に行うことができます。私もこの最適化を適用する場合はありますが、それは execute() メソッドが何らかの形でボトルネックまたはリソースの浪費になっていることがわかった場合のみです。やみくもに最適化してはいけません。
|
|
次に、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_timeline を Scitter クラスに追加するには少し面倒な部分があります。friends_timeline という呼び出しは、返される結果を制御する一連の「オプション・パラメーター」(since_id、max_id、count、page) を受け付けます。ところが、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() メソッドとまったく同じように見え、また動作もほとんど同じです。
 |
今後について
私はこの記事を書く中で、『Programming in Scala』の著者である Scala Bill Venners と Martin Odersky、そして Lex Spoon に対して、オプション・パラメーターを処理する最高の方法、あるいは最も Scala らしい方法について、彼らの考え方を尋ねました。その議論の中で Martin は、彼らがオプション・パラメーターの処理について検討中であり、おそらく 2.8 リリースで対応できるだろうと語りました。それが「デフォルト・パラメーター」という形なのか、あるいは「オプション・パラメーター」という形なのかは (少なくとも私には) まだ明確ではありませんが、このことに特に関心がある人は、今がメーリング・リストに参加する絶好のチャンスです。
|
|
しかしこれでもまだ、オプション・パラメーターの問題が残っています。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) の言語の方が常に優れていることを忘れないでください (どうも私はこの冗談を言いすぎるようで申し訳ありません)。
参考文献 学ぶために
製品や技術を入手するために
議論するために
著者について  | 
|  | Ted Neward は世界規模でコンサルティングを行う ThoughtWorks のコンサルタントであり、また Neward & Associates の代表として、Java や .NET、XML サービスなどのプラットフォームに関するコンサルティング、助言、指導、講演を行っています。彼はワシントン州シアトルの近郊に在住しています。 |
記事の評価
|