程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 面向Java開發人員的Scala指南 - 用Scitter更新Twitter

面向Java開發人員的Scala指南 - 用Scitter更新Twitter

編輯:關於JAVA

在撰寫本文時,夏季即將結束,新的學年就要開始,Twitter 的服務器上不 斷湧現出世界各地的網蟲和非網蟲們發布的更新。對於我們很多身在北美的人來 說,從海灘聚會到足球,從室外娛樂到室內項目,各種各樣的想法紛至沓來。為 了跟上這種形勢,是時候重訪 Scitter 這個用於訪問 Twitter 的 Scala 客戶 機庫了。

如果 到目前為止 您一直緊隨 Scitter 的開發,就會知道,這個庫現在能夠 利用各種不同的 Twitter API 查看用戶的好友、追隨者和時間線,以及其他內 容。但是,這個庫還不具備發布狀態更新的能力。在這最後一篇關於 Scitter 的文章中,我們將豐富這個庫的功能,增加一些有趣的內容(終止和評價)功能 和重要方法 update()、show() 和 destroy()。在此過程中,您將了解更多關於 Twitter API 的知識,它與 Scala 之間的交互如何,您還將了解如何克服兩者 之間不可避免的編程挑戰。

注意,當您看到本文的時候,Scitter 庫將位於一個 公共源代碼控制庫 中 。當然,我還將在本文中包括 源代碼,但是要知道,源代碼庫可能發生改變。 換句話說,項目庫中的代碼與您在這裡看到的代碼可能略有不同,或者有較大的 不同。

POST 到 Twitter

到目前為止,我們的 Scitter 開發主要集中於一些基於 HTTP GET 的操作, 這主要是因為這些調用非常容易,而我想輕松切入 Twitter API。將 POST 和 DELETE 操作添加到庫中對於可見性來說邁出了重要一步。到目前為止,可以在 個人 Twitter 帳戶上運行單元測試,而其他人並不知道您要干什麼。但是,一 旦開始發送更新消息,那麼全世界都將知道您要運行 Scitter 單元測試。

如果繼續測試 Scitter,那麼需要在 Twitter 上創建自己的 “測試” 帳戶 。(也許用 Twitter API 編程的最大缺點是沒有任何合適的測試或模擬工具。 )

目前的進展

在開始著手這個庫的新的 UPDATE 功能之前,我們來回顧一下到目前為止我 們已經創建的東西。

大致來說,Scitter 庫分為 4 個部分:

來回發送的請求和響應類型(User、Status 等),包含在 API 中;它們被 建模為 case 類。

OptionalParam 類型,同樣在 API 中的某些地方;也被建模為 case 類,這 些 case 類繼承基本的 OptionalParam 類型。

Scitter 對象,用於通信基礎和對 Twitter 的匿名(無身份驗證)訪問。

Scitter 類,存放一個用戶名和密碼,用於訪問給定 Twitter 帳戶時進行驗 證。

注意,在這最後一篇文章中,為了使文件大小保持在相對合理的范圍內,我 將請求/響應類型分開放到不同的文件中。

終止和評價

那麼,現在我們清楚了目標。我們將通過實現兩個 “只讀” Twitter API 來達到目標:end_session API(結束用戶會話)和 rate_limit_status API( 描述在某一特定時段內用戶帳戶還剩下多少可用的 post)。

end_session API 與它的同胞 verify_credentials 相似,也是一個非常簡 單的 API:只需用一個經過驗證的請求調用它,它將 “結束” 當前正在運行的 會話。在 Scitter 類上實現它非常容易,如清單 1 所示:

清單 1. 在 Scitter 上實現 end_session

package  com.tedneward.scitter

{

  import org.apache.commons.httpclient._, auth._, methods._,  params._

  import scala.xml._

  // ...

  class Scitter

  {

   /**

    *

    */ 

   def endSession : Boolean =

   {

    val (statusCode, statusBody) =

     Scitter.execute ("http://twitter.com/account/end_session.xml",

      username, password)

    statusCode == 200

   }

  }

}

好吧,我失言了。也不是那麼容易。

POST

和我們到目前為止用過的 Twitter API 中的其他 API 不一樣,end_session 要求傳入的消息是用 HTTP POST 語義發送的。現在,Scitter.execute 方法做 任何事情都是通過 GET,這意味著需要將那些期望 GET 的 API 與那些期望 POST 的 API 區分開來。

現在暫不考慮這一點,另外還有一個明顯的變化:POST 的 API 調用還需將 名稱/值對傳遞到 execute() 方法中。(記住,在其他 API 調用中,若使用 GET,則所有參數可以作為查詢參數出現在 URL 行;若使用 POST,則參數出現 在 HTTP 請求的主體中。)在 Scala 中,每當提到名稱/值對,自然會想到 Scala Map 類型,所以在考慮建模作為 POST 一部分發送的數據元素時,最容易 的方法是將它們放入到一個 Map[String,String] 中並傳遞。

例如,如果將一個新的狀態消息傳遞給 Twitter,需要將這個不超過 140 個 字符的消息放在一個名稱/值對 status 中,那麼應該如清單 2 所示:

清單 2. 基本 map 語法

val map = Map("status" -> message)

在此情況下,我們可以重構 Scitter.execute() 方法,使之用 一個 Map 作 為參數。如果 Map 為空,那麼可以認為應該使用 GET 而不是 POST,如清單 3 所示:

清單 3. 重構 execute()

private[scitter] def execute(url  : String) : (Int, String) =

    execute(url, Map(), "", "")

   private[scitter] def execute(url : String, username :  String,

                password : String) : (Int,  String) =

    execute(url, Map(), username, password)

   private[scitter] def execute(url : String,

                dataMap : Map[String,String]) :  (Int, String) =

    execute(url, dataMap, "", "")

   private[scitter] def execute(url : String, dataMap :  Map[String,String],

                  username : String, password :  String) =

   {

    val client = new HttpClient()

    val method =

     if (dataMap.size == 0)

     {

      new GetMethod(url)

     }

     else

     {

      var m = new PostMethod(url)

      val array = new Array[NameValuePair](dataMap.size)

      var pos = 0

      dataMap.elements.foreach { (pr) =>

       pr match {

        case (k, v) => array(pos) = new  NameValuePair(k, v)

       }

       pos += 1

      }

      m.setRequestBody(array)

      m

     }

    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())

   }

execute() 方法最大的變化是引入了 Map[String,String] 參數,以及與它 的大小有關的 “if” 測試。該測試決定是處理 GET 請求還是 POST 請求。由 於 Apache Commons HttpClient 要求 POST 請求的主體放在 NameValuePairs 中,因此我們使用 foreach() 調用遍歷 map 的元素。我們以二元組 pr 的形式 傳入 map 的鍵和值,並將它們分別提取到本地綁定變量 k 和 v,然後使用這些 值作為 NameValuePair 構造函數的構造函數參數。

我們還可以使用 PostMethod 上的 setParameter(name, value) API 更輕松 地做這些事情。出於教學的目的,我選擇了清單 3 中的方法:以表明 Scala 數 組和 Java 數組一樣,仍然是可變的,即使數組引用被標記為 val 仍是如此。 記住,在實際代碼中,對於每個 (k,v) 元組,使用 PostMethod 上的 setParameter(name, value) 方法要好得多。

還需注意,對於 if/else 返回的 “method” 對象的類型,Scala 編譯器會 進行 does the right thing 類型推斷。由於 Scala 可以看到 if/else 返回的 是 GetMethod 還是 PostMethod 對象,它會選擇最接近的基本類型 HttpMethodBase 作為 “method” 的返回類型。這也意味著,在 execute() 方 法的其余部分中,HttpMethodBase 中的任何不可用方法都是不可訪問的。幸運 的是,我們不需要它們,所以至少現在沒有問題。

清單 3 中的實現的背後還潛藏著最後一個問題,這個問題是由這樣一個事實 引起的:我選擇了使用 Map 來區分 execute() 方法是處理 GET 操作,還是處 理 POST 操作。如果還需要使用其他 HTTP 動作(例如 PUT 或 DELETE),那麼 將不得不再次重構 execute()。到目前為止,還沒有這樣的問題,但是今後要記 住這一點。

測試

在實施這樣的重構之前,先運行 ant test,以確保原有的所有基於 GET 的 請求 API 仍可使用 — 事實確實如此。(這裡假設生產 Twitter API 或 Twitter 服務器的可用性沒有變化)。一切正常(至少在我的計算機上是這樣) ,所以實現新的 execute() 方法就非常容易:

清單 4. Scitter v0.3: endSession

def endSession :  Boolean =

   {

    val (statusCode, statusBody) =

     Scitter.execute ("http://twitter.com/account/end_session.xml",

      Map("" -> ""), username, password)

    statusCode == 200

   }

這實在是再簡單不過了。

接下來要做的是實現 rate_limit_status API,它有兩個版本,一個是經過 驗證的版本,另一個是沒有經過驗證的版本。我們將該方法實現為 Scitter 對 象和 Scitter 類上的 rateLimitStatus,如清單 5 所示:

清單 5. Scitter v0.3: rateLimitStatus

package  com.tedneward.scitter

{

  object Scitter

  {

   // ...

   def rateLimitStatus : Option[RateLimits] =

   {

    val url =  "http://twitter.com/account/rate_limit_status.xml" 

    val (statusCode, statusBody) =

     Scitter.execute(url)

    if (statusCode == 200)

    {

     Some(RateLimits.fromXml(XML.loadString(statusBody)))

    }

    else

    {

     None

    }

   }

  }

  class Scitter

  {

   // ...

   def rateLimitStatus : Option[RateLimits] =

   {

    val url =  "http://twitter.com/account/rate_limit_status.xml" 

    val (statusCode, statusBody) =

     Scitter.execute(url, username, password)

    if (statusCode == 200)

    {

     Some(RateLimits.fromXml(XML.loadString(statusBody)))

    }

    else

    {

     None

    }

   }

  }

}

我覺得還是很簡單。

更新

現在,有了新的 POST 版本的 HTTP 通信層,我們可以來處理 Twitter API 的中心:update 調用。毫不奇怪,需要一個 POST,並且至少有一個參數,即 status。

status 參數包含要發布到認證用戶的 Twitter 提要的不超過 140 個字符的 消息。另外還有一個可選參數:in_reply_to_status_id,該參數提供另一個更 新的 id,執行了 POST 的更新將回復該更新。

update 調用差不多就是這樣了,如清單 6 所示:

清單 6. Scitter v0.3: update

package  com.tedneward.scitter

{

  class Scitter

  {

   // ...

   def update(message : String, options : OptionalParam*) :  Option[Status] =

   {

    def optionsToMap(options : List[OptionalParam]) : Map [String, String]=

    {

     options match 

     {

      case hd :: tl =>

       hd match {

        case InReplyToStatusId(id) =>

         Map("in_reply_to_status_id" -> id.toString)  ++ optionsToMap(tl)

        case _ =>

         optionsToMap(tl)

       }

      case List() => Map()

     }

    }

    val paramsMap = Map("status" -> message) ++  optionsToMap(options.toList)

    val (statusCode, body) =

     Scitter.execute ("http://twitter.com/statuses/update.xml",
       paramsMap, username, password)

    if (statusCode == 200)

    {

     Some(Status.fromXml(XML.loadString(body)))

    }

    else

    {

     None

    }

   }

  }

}

也許這個方法中最 “不同” 的部分就是其中定義的嵌套函數 — 與使用 GET 的其他 Twitter API 調用不同,Twitter 期望傳給 POST 的參數出現在執 行 POST 的主體中,這意味著在調用 Scitter.execute() 之前需要將它們轉換 成 Map 條目。但是,默認的 Map(來自 scala.collections.immutable)是不 可變的,這意味著可以組合 Map,但是不能將條目添加到已有的 Map 中。

可變集合

在 Scala 中可以使用可變集合,只需導入 scala.collections.mutable 包 ,而不是 scala.collections.immutable。但是,這樣做要冒常見的 使用可變 數據 的風險,所以常規的 Scala 編程風格建議從其他不可變集合創建不可變集 合。如果確實需要或想要使用可變集合,那麼可以導入 scala.collections,然 後用部分包名前綴按 mutable.Map 或 immutable.Map 的方式引用 Map(或其他 )集合類型。這樣可以避免在一個塊中同時使用可變集合和不可變集合時出現混 淆。

解決這個小難題的最容易的方法是遞歸地處理傳入的 OptionalParam 元素的 列表(實際上是一個 Array[])。我們將每個元素拆開,將它轉換成各自的 Map 條目。然後,將一個新的 Map(由新創建的 Map 和從遞歸調用返回的 Map 組成 )返回到 optionsToMap。

然後,將 OptionalParam 的 Array[] 傳遞到 optionsToMap 嵌套函數。然 後,將返回的 Map 與我們構建的包含 status 消息的 Map 連接起來。最後,將 新的 Map 和用戶名、密碼一起傳遞給 Scitter.execute() 方法,以傳送到 Twitter 服務器。

隨便說一句,所有這些任務需要的代碼並不多,但是需要更多的解釋,這是 比較優雅的編程方式。

潛在的重構

理論上,傳給 update 的可選參數與傳給其他基於 GET 的 API 調用的可選 參數將受到同等對待;只是結果的格式有所不同(結果是用於 POST 的名稱/值 對,而不是用於 URL 的名稱/值對)。

如果 Twitter API 需要其他 HTTP 動作支持(PUT 和/或 DELETE 就是可能 需要的動作),那麼總是可以將 HTTP 參數作為特定參數 — 也許又是一組 case 類 — 並讓 execute() 以一個 HTTP 動作、URL、名稱/值對的 map 以及 (可選)用戶名/密碼作為 5 個參數。然後,必要時可以將可選參數轉換成一個 字符串或一組 POST 參數。這些內容只需記在腦中就行了。

顯示

show 調用接受要檢索的 Twitter 狀態的 id,並顯示 Twitter 狀態。和 update 一樣,這個方法非常簡單,無需再作說明,如清單 7 所示:

清單 7. Scitter v0.3: show

package  com.tedneward.scitter

{

  class Scitter

  {

   // ...

   def show(id : Long) : Option[Status] =

   {

    val (statusCode, body) =

     Scitter.execute("http://twitter.com/statuses/show/" + id  + ".xml",

   username, password)

    if (statusCode == 200)

    {

     Some(Status.fromXml(XML.loadString(body)))

    }

    else

    {

     None

    }

   }

  }

}

還有問題嗎?

另一種顯示方法

如果想再試一下模式匹配,那麼可以看看清單 8 中是如何以另一種方式編寫 show() 方法的:

清單 8. Scitter v0.3: show redux

package  com.tedneward.scitter

{

  class Scitter

  {

   // ...

   def show(id : Long) : Option[Status] =

   {

    Scitter.execute("http://twitter.com/statuses/show/" + id +  ".xml",
      username, password) match 

    {

     case (200, body) =>

      Some(Status.fromXml(XML.loadString(body)))

     case (_, _) =>

      None

    }

   }

  }

}

這個版本比起 if/else 版本是否更加清晰,這很大程度上屬於審美的問題, 但公平而論,這個版本也許更加簡潔。(很可能查看代碼的人看到 Scala 的 “ 函數” 部分越多,就認為這個版本越吸引人。)

但是,相對於 if/else 版本,模式匹配版本有一個優勢:如果 Twitter 返 回新的條件(例如不同的錯誤條件或來自 HTTP 的響應代碼),那麼模式匹配版 本在區分這些條件時可能更清晰。例如,如果某天 Twitter 決定返回 400 響應 代碼和一條錯誤消息(在主體中),以表明某種格式錯誤(也許是沒有正確地重 新 Tweet),那麼與 if/else 方法相比,模式匹配版本可以更輕松(清晰)地 同時測試響應代碼和主體的內容。

還應注意,我們還可以使用清單 8 中的方式創建一些局部應用的函數,這些 函數只需要 URL 和參數。但是,坦白說,這是一種自找麻煩的解放方案,所以 我不會采用。

撤銷

我們還想讓 Scitter 用戶可以撤銷剛才執行的動作。為此,需要一個 destroy 調用,它將刪除已發布的 Twitter 狀態,如清單 9 所示:

清單 9. Scitter v0.3: destroy

package  com.tedneward.scitter

{

  class Scitter

  {

   // ...

   def destroy(id : Long) : Option[Status] =

   {

    val paramsMap = Map("id" -> id.toString())

    val (statusCode, body) =

     Scitter.execute("http://twitter.com/statuses/destroy/" +  id.toString() + ".xml",

      paramsMap, username, password)

    if (statusCode == 200)

    {

     Some(Status.fromXml(XML.loadString(body)))

    }

    else

    {

     None

    }

   }

   def destroy(id : Id) : Option[Status] =

    destroy(id.id.toLong)

  }

}

有了這些東西,我們可以考慮將這個 Scitter 客戶機庫作為 “alpha” 版 ,至少實現一個簡單的 Scitter 客戶機。(按照慣例,這個任務就留給您來完 成,作為一項 “讀者練習”。)

結束語

編寫 Scitter 客戶機庫是一項有趣的工作。雖然不能說 Scitter 已經可以 完全用於生產,但是它絕對足以用於實現簡單的、基於文本的 Twitter 客戶機 ,這意味著它已經可以投入使用了。要發現什麼人可以使用它,哪些特性是需要 的,從而使之變得更有用,最好的方法就是將它向公眾發布。

我已經將本文和之前關於 Scitter 的文章中的代碼作為第一個修訂版提交到 Google Code 上的 Scitter 項目主頁。歡迎下載和試用這個庫,並告訴我您的 想法。同時也歡迎提供 bug 報告、修復和建議。

您也無需受我的代碼庫的束縛。見證了之前三篇文章中進行的 Scitter 開發 ,您應該對 Twitter API 的使用有很好的理解。如果對於使用該 API 有不同的 想法,那麼盡管去做:拋開 Scitter,構建自己的 Scala 客戶機庫。畢竟,做 做這些內部項目也是挺有樂趣的。

現在,我們要向 Scitter 揮手告別,開始尋找新的用 Scala 解決的項目。 願您從中找到樂趣,如果發現了用 Scala 編程的工作,別忘了告訴我!

文章來源:

http://www.ibm.com/developerworks/cn/java/j-scala10209.html

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved