歡迎您光臨本站 註冊首頁

Java開發者的Scala指南: Scala Twitter=Scitter

←手機掃碼閱讀     火星人 @ 2014-03-09 , reply:0

  抽象地討論 Scala 是一件有趣的事情,但對於本專欄的大多數讀者而言,需要通過實踐才能理解理論和應用之間的區別.在本期文章中,Ted Neward 將使用 Scala 為客戶構建基礎框架,用於訪問流行的微型博客系統 Twitter.

  Twitter 迅速佔領了 Internet 市場.您肯定知道這個出色的社交網路工具允許訂閱者提供關於他們自身以及當前正在執行的任務的簡要狀態更新.追隨者將接收到他們的 「Twitter 提要」 的更新,這與博客將更新生成到博客閱讀者的提要中極為類似.

  關於本系列

  Ted Neward 將深入探討 Scala 編程語言,並帶領您一路隨行.在本 系列 中,您將學習最新的熱點以及 Scala 的語言功能.Scala 代碼和 Java™ 代碼將在必要時同時出現以方便進行比較,但您會發現 Scala 中的許多內容都與 Java 沒有直接關係 — 這便是 Scala 的魄力所在!畢竟,如果 Java 能夠做到,為什麼還要大費周折來學習 Scala 呢?

  就其本身而言,Twitter 是對社交網路的有趣討論,並且是用戶之間的新一代 「高度互聯」,它具備您能想到的所有優點和缺點.

  由於 Twitter 很早就發布了其 API,因此大量 Twitter 客戶機應用程序湧入到 Internet 上.由於該 API 主要建立在直觀和易於理解的基礎上,因此許多開發人員都發現有必要構建一個自己的 Twitter 客戶機,這與學習 Web 技術的開發人員構建自己的博客伺服器極為類似.

  考慮到 Scala 的功能性(這看上去能很好地協同 Twitter 的 REST 式特性)以及非常出眾的 XML 處理特性,因此嘗試構建一個用於訪問 Twitter 的 Scala 客戶機庫應該是一個非常不錯的體驗.

  何為 Twitter?

  在詳細討論之前,我們先來看看 Twitter API.

  簡單來說,Twitter 是一個 「微型博客」 — 關於您自己的簡短個性化提要,不超過 140 個字元,任何 「追隨者」 都可以通過 Web 更新、RSS、文本消息等方式接收它們.(140 字元的限制完全來自文本消息,它是 Twitter 的主要來源渠道,並受到類似的限制).

  最具 REST 特徵 是什麼意思?

  一些讀者會對我所使用的最具 REST 特徵 短語感到好奇;這需要一些說明.「Twitter API 試圖符合 Representational State Transfer (REST) 的設計原則」.並且在很大程度上說它做到了.該思想的創造者 Roy Fielding 可能不同意 Twitter 使用這個術語,但實現來說,Twitter 的方法將適合大多數人的 REST 定義.我只希望避免關於 REST 定義的激烈爭論.因此,我使用了限定詞 「最」.

  從實際的角度來說,Twitter 是一個最具 REST 特徵 的 API,您可以使用一些種類的消息格式 — XML、ATOM、RSS 或 JSON — 來發送或從 Twitter 伺服器接收消息.不同的 URL,與不同的消息和它們所需及可選的消息部分相結合,可以發起不同的 API 調用.例如,如果您希望接收 Twitter 上所有人的所有 「Tweets」(Twitter 更新)的完整列表(也稱作 「公共時間軸」),您需要準備一個 XML、ATOM、RSS 或 JSON 消息,將它發送給合適的 URL,並採用與 Twitter 網站(apiwiki.twitter.com)上相同的格式來使用結果:

  ------------------------------------------------------------

  public_timeline

  返回設定了自定義用戶圖標的

  非保護用戶的 20 條最新狀態.不需要身份驗證.

  注意,公共時間軸將緩存 60 秒鐘

  因此頻繁請求它不再浪費資源.

  URL: http://twitter.com/statuses/public_timeline.format

  格式:xml、json、rss、atom

  方法:GET

  API 限制:不適用

  返回:狀態元素列表

  ------------------------------------------------------------

  從編程的角度來說,這意味著我們給 Twitter 伺服器發送一個簡單的 GET HTTP 請求,並且我們將獲取一組封裝在 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>

  部的話)都很直觀,因此不再贅述.

  由於我們可以採用三種基於 XML 的格式使用 Twitter 消息,以及 Scala 具備一些非常強大的 XML 特性,包括 XML 字面值和類似 XPath 的查詢語法 API,因此編寫可以發送和接收 Twitter 消息的 Scala 庫只需要一些基礎的 Scala 編碼工作.舉例來說,通過 Scala 使用清單 1 消息來提取狀態更新的標題或內容可以利用 Scala 的 XML 類型和 及 \ 方法,如清單 2 所示:

  清單 2. 您好 Ted,您在哪裡?


<![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 消息發送和接收任務的類.作為其中一個目標,應該能夠在 「普通」 Java 程序中方便地使用庫(這意味著可以方便地從任何可理解普通 Java 語義的環境中來訪問它,比如說 Groovy 或 Clojure).

  API 設計

  在深入了解 Scala/Twitter 庫的 API 設計之前(根據同事 ThoughtWorker Neal Ford 的建議,我將它稱作 「Scitter」),需要明確一些需求.

  ,Scitter 顯然會對網路訪問有一些依賴 — 並且可擴展到 Twitter 伺服器 — 這會使測試變得非常困難.

  其次,我們需要解析(和測試)Twitter 發回的各種格式.

  第三,我們希望隱藏 API 內部各種格式之間的差異,以便客戶機不需要擔心已記錄的 Twitter 消息格式,但是可以僅使用標準類.

  ,由於 Twitter 依賴 「通過身份驗證的用戶」 才能使用大量 API,因此 Scitter 庫需要適應 「驗證」 和 「未驗證」 API 之間的差異,而不會讓事情變得過於複雜.

  網路訪問需要一些形式的 HTTP 通信,以便聯繫 Twitter 伺服器.雖然我們可以使用 Java 庫本身(特別是 URL 類及其同胞),但由於 Twitter API 需要大量請求和響應主體連接,因此可以更加輕鬆地使用不同的 HTTP API,特別是 Apache Commons HttpClient 庫.為了更便於測試客戶機 API,實際通信將隱藏在一些 API 內部的 Scitter 庫中,以便能夠更加輕鬆地切換到另一個 HTTP 庫(其必要性不太容易想到),並能模擬實際網路通信以簡化測試(其作用很容易想到).

  結果,第一個測試是 Scala 化 HttpClient 調用,以確保基本通信模式就位;注意,由於 HttpClient 依賴另外兩個 Apache 庫(Commons Logging 和 Commons Codec),因此還需要在運行時提供這些庫;對於那些希望開發相似種類代碼的讀者,確保類路徑中包括所有三個庫.

  由於最易於使用的 Twitter API 是測試 API

  因此在請求格式中返回 「ok」,並附帶 200 OK HTTP 狀態碼.

  我們將使用它作為 Scitter 測試中的保留條款.它位於 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 文檔了解詳細信息.假設連接到公共 Internet 的網路可用(並且 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.再次,HttpClient 代碼將類似於之前的代碼,除了將用戶名和密碼傳遞到 Twitter API 中之外.

  這引出了 Twitter 如何驗證用戶的概念.快速查看 Twitter API 頁面后,可以發現 Twitter 使用的是一種 Stock 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 帳戶相當簡單,因此我假定您已經擁有一個帳戶,或者知道如何註冊(很好.我會等待完成此任務).

  完成後,使用用戶名和密碼構造函數參數將它映射到 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
  }
  }
  }

  清單 7 中相應的 Scitter 類測試也相當簡單:

  清單 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 到對象

  現在可以添加的最簡單的 API 是 public_timeline,它收集 Twitter 從所有用戶處接收到的最新的 n 更新,並返回它們以便於進行使用.與之前討論的另外兩個 API 不同,public_timeline API 返回一個響應主體(而不是僅依賴於狀態碼),因此我們需要分解生成的 XML/RSS/ATOM/,然後將它們返回給 Scitter 客戶機.

  現在,我們編寫一個探索測試,它將訪問公共提要並將結果轉儲到 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 伺服器上有許多用戶,但通常應與清單 9 的 JUnit 文本文件轉儲類似:

  清單 9. 我們的 Tweets 結果


 <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 文檔可以看出,調用的結果是一組具備一致消息結構的簡單 「狀態」 消息.使用 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 { contents @ _*} =>
  {
  System.out.println("Status: ")
  contents.foreach((c) =>
  c match
  {
  case { t @ _*} =>
  System.out.println("tText: " t.text.trim)
  case { contents2 @ _* } =>
  {
  contents2.foreach((c2) =>
  c2 match
  {
  case { u } =>
  System.out.println("tUser: " u.text.trim)
  case _ =>
  ()
  }
  )
  }
  case _ =>
  ()
  }
  )
  }
  case _ =>
  () // or, if you prefer, System.out.println("Unrecognized element!")
  }
  }
  }
  }
  }

  隨著示例代碼模式的變化,這並不值得推薦 — 這有點類似於 DOM,依次導航到各個子元素,提取文本,然後導航到另一個節點.我可以僅執行兩個 XPath 樣式的查詢,如清單 11 所示:

  清單 11. 替代解析方法


 for (n <- statuses.elements)
  {
  val text = (n \ "text").text
  val screenName = (n \ "user" "screen_name").text
  }

  這顯然更加簡短,但它帶來了兩個基本問題:

  我們可以強制 Scala 的 XML 庫針對每個元素或子元素遍歷一次圖,其速度會隨時間減慢.

  我們仍然需要直接處理 XML 消息的結構.這是兩個問題中最為重要的.

  也就是說,這種方式不具備可伸縮性 — 假設我們最終對 Twitter 狀態消息中的每個元素都感興趣,我們將需要分別從各狀態中提取各個元素.

  這又造成了另一個與各格式本身相關的問題.記住,Twitter 可以使用四種不同的格式,並且我們不希望 Scitter 客戶機需要了解它們之間的任何差異,因此 Scitter 需要一個能返回給客戶機的中間結構,以便未來使用,如清單 12 所示:

  清單 12. Breaker,您的狀態是什麼?


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 方式相類似,考慮到簡潔性,我就不再重複了.注意,User 子元素有一個有趣的問題 — 雖然存在 Twitter 用戶類型,但其中內嵌了一個可選的 「最新狀態」.狀態消息還內嵌了一個用戶.對於這種情況,為了幫助避免一些潛在的遞歸問題,我選擇創建一個嵌入在 Status 內部的 User 類型,以反映所出現的 User 數據;反之亦然,Status 也可以嵌入在 User 中,這樣可以明確避免該問題.(至少,在沒發現問題之前,這種方法是有效的).

  現在,創建了表示 Twitter 消息的對象類型之後,我們可以遵循 XML 反序列化的公共 Scala 模式:創建相應的對象定義,其中包含一個 fromXml 方法,用於將 XML 節點分離到對象實例中,如清單 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 支持的其他任何格式進行擴展 — fromXml 方法可以在分解節點之前檢查它是否保存了 XML、RSS 或 Atom 類型的內容,或者 Status 可以包含 fromXml、fromRss、fromAtom 和 fromJson 方法.實際上,后一種方法是我的優先選擇,它會平等對待基於 XML 的格式和 JSON(基於文本)格式.

  好奇和細心的讀者會注意到在 Status 及其內嵌 User 的 fromXml 方法中,我使用的是 XPath 樣式的分解方法,而不是之前建議的遍歷內嵌元素的方法.現在,XPath 樣式的方法看上去更易於閱讀,但幸運的是,我後來改變了注意,良好的封裝仍然是我的朋友 — 我可以在隨後修改它,Scitter 外部的任何人都不會知道.

  注意 Status 內部的兩個成員如何使用 Option[T] 類型;這是這些元素通常排除在 Status 消息外部,並且雖然元素本身會出現,但它們顯示為空(類似於 ).這正是 Option[T] 的作用所在.當元素為空時,它們將使用 「None」 值.(這表示考慮到基於 Java 的兼容性,訪問它們會更加困難,但惟一可行方法是對最終生成的 Option 實例調用 get(),這不太複雜並且能很好地解決 「非 null 即 0」 問題).

  現在已經可以輕而易舉地使用公共時間軸:

  清單 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
  }
  }
  }
  }

  在實現功能全面的 Twiter 客戶機之前,我們顯然還有很長的路要走.但到目前為止,我們已經實現基本的行為.

  結束語

  構建 Scitter 庫的工作進展順利;目前,Scitter 測試實現相對比較簡單,與產生 Scitter API 的探索測試相比時尤為如此.外部用戶不需要擔心 Twitter API 或者它的各種格式的複雜性,雖然目前測試 Scitter 庫有點困難(對單元測試而言,依賴網路並不是個好方法),但我們會及時解決此問題.

  注意,我故意在 Twitter API 中維持了面向對象的感覺,秉承了 Scala 的精神 — Scala 支持大量功能特性並不表示我們要放棄 Java 結構採用的對象設計方法.我們將接受有用的功能特性,同時仍然保留適用的 「舊方法」.

  這並不是說我們在此處提供的設計是解決問題最好的方法,只能說這是我們決定採用的設計方法;並且,我是本文的作者,我採用的是自己的方式.如果不喜歡,您可以編寫自己的庫和文章(並將 URL 發送給我,我會在未來的文章中向您發起挑戰).事實上,在未來的文章中,我會將所有這些封裝在一個 Scala 「sbaz」 包中,並上傳到網上供大家下載.

  現在,我們又要暫時說再見了.下個月,我將在 Scitter 庫中添加更多有趣的特性,並開始考慮如何簡化它的測試和使用.


[火星人 ] Java開發者的Scala指南: Scala Twitter=Scitter已經有864次圍觀

http://coctec.com/docs/java/show-post-61062.html