抽象地谈论 Scala 的确有趣然而一旦将其付诸实践就会发现将它作为 玩具 与在工作中使用它的区别Scala 狂热者 Ted Neward 撰写了一篇 对 Scitter 的介绍Scitter 是一个用于访问 Twitter 的 Scala 库本文是其后续篇在本文中Ted Neward 为这个客户机库提供了一组更有趣也更有用的特性
欢迎回来Scala 迷们上个月我们谈到了 Twitter这个微博客站点目前正引起社会性网络的极大兴趣我们还谈到它的基于 XML/REST 的 API 如何使它成为开发人员进行研究和探索的一个有趣平台为此我们首先充实了 Scitter 的基本结构Scitter 是用于访问 Twitter 的一个 Scala 库
我们对于 Scitter 有几个目标
● 简化 Twitter 访问比过去打开 HTTP 连接然后 手动 执行操作更容易
● 可以从 Java 客户机轻松访问它
● 轻松模拟以便进行测试
在这一期我们不必完成整个 Twitter API但是我们将完成一些核心部分目的是让这个库达到公共源代码控制库的程度便于其他人来完成这项工作
到目前为止Scitter
首先我们简单回顾一下到目前为止我们所处的阶段
清单 Scitter v
package comtednewardscitter
{
import mons_ auth_ methods_ params_
import scalaxml_
/**
* 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 thats what its there for
*/
abstract class Status
{
/**
* Nested User type This could be combined with the toplevel User type
* if we decide later that its OK for this to have a boatload of optional
* elements including the mostrecentlyposted 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)texttoLong
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)texttoBoolean
val followersCount = (node \ followers_count)texttoInt
}
}
}
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)texttoLong
val text = (node \ text)text
val source = (node \ source)text
val truncated = (node \ truncated)texttoBoolean
val inReplyToStatusId =
if ((node \ in_reply_to_status_id)text != )
Some((node \in_reply_to_status_id)texttoLong)
else
None
val inReplyToUserId =
if ((node \ in_reply_to_user_id)text != )
Some((node \in_reply_to_user_id)texttoLong)
else
None
val favorited = (node \ favorited)texttoBoolean
val user = UserfromXml((node \ user)())
}
}
}
/**
* Object for consuming nonspecific Twitter feeds such as the public timeline
* Use this to do nonauthenticated requests of Twitter feeds
*/
object Scitter
{
/**
* Ping the server to see if its up and running
*
* Twitter docs say:
* test
* Returns the string ok in the requested format with a OK HTTP status code
* URL:
* Formats: xml json
* Method(s): GET
*/
def test : Boolean =
{
val client = new HttpClient()
val method = new GetMethod()
methodgetParams()setParameter(HttpMethodParamsRETRY_HANDLER
new DefaultHttpMethodRetryHandler( false))
clientexecuteMethod(method)
val statusLine = methodgetStatusLine()
statusLinegetStatusCode() ==
}
/**
* Query the public timeline for the most recent statuses
*
* Twitter docs say:
* public_timeline
* Returns the most recent statuses from nonprotected users who have set
* a custom user icon Does not require authentication Note that the
* public timeline is cached for seconds so requesting it more often than
* that is a waste of resources
* URL: _timelineformat
* Formats: xml json rss atom
* Method(s): GET
* API limit: Not applicable
* Returns: list of status elements
*/
def publicTimeline : List[Status] =
{
import llectionmutableListBuffer
val client = new HttpClient()
val method = new GetMethod(_timelinexml)
methodgetParams()setParameter(HttpMethodParamsRETRY_HANDLER
new DefaultHttpMethodRetryHandler( false))
clientexecuteMethod(method)
val statusLine = methodgetStatusLine()
if (statusLinegetStatusCode() == )
{
val responseXML =
XMLloadString(methodgetResponseBodyAsString())
val statusListBuffer = new ListBuffer[Status]
for (n < (responseXML \\ status)elements)
statusListBuffer += (StatusfromXml(n))
statusListBuffertoList
}
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 OK response code and a representation of the
* requesting user if authentication was successful; returns a status
* code and an error message if not Use this method to test if supplied
* user credentials are valid
* URL: _credentialsformat
* Formats: xml json
* Method(s): GET
*/
def verifyCredentials : Boolean =
{
val client = new HttpClient()
val method = new GetMethod()
methodgetParams()setParameter(HttpMethodParamsRETRY_HANDLER
new DefaultHttpMethodRetryHandler( false))
clientgetParams()setAuthenticationPreemptive(true)
val creds = new UsernamePasswordCredentials(username password)
clientgetState()setCredentials(
new AuthScope( AuthScopeANY_REALM) creds)
clientexecuteMethod(method)
val statusLine = methodgetStatusLine()
statusLinegetStatusCode() ==
}
}
}
代码有点长但是很容易分为几个基本部分
● case 类 User 和 Status表示 Twitter 在对 API 调用的响应中发回给客户机的基本类型包括用于构造或提取 XML 的一些方法
● 一个 Scitter 独立对象处理那些不需要对用户进行验证的操作
● 一个 Scitter 实例(用 username 和 password 参数化)用于那些需要对用户执行验证的操作
到目前为止对于这两种 Scitter 类型我们只谈到了测试verifyCredentials 和 public_timeline API虽然这些有助于确定 HTTP 访问的基础(使用 Apache HttpClient 库)可以工作并且我们将 XML 响应转换成 Status 对象的基本方式也是可行的但是现在我们甚至不能进行基本的 我的朋友在说什么 的公共时间线查询也没有采取过基本的措施来防止代码库中出现 重复 问题更不用说寻找一些方法来模拟用于测试的网络访问代码
显然在这一期我们有很多事情要做
连接
对于代码第一件让我烦恼的事就是我在 Scitter 对象和类的每个方法中都重复这样的操作序列创建 HttpClient 实例对它进行初始化用必要的验证参数对它进行参数化等等当它们只有 个方法时可以进行管理但是显然不易于伸缩而且以后还会增加很多方法此外以后重新在那些方法中引入模拟和/或本地/离线测试功能将十分困难所以我们要解决这个问题
实际上我们这里介绍的并不是 Scala 本身而是不要重复自己(DontRepeatYourself)的思想因此我将从基本的面向对象方法开始创建一个 helper 方法用于做实际工作
清单 对代码库执行 DRY 原则
package comtednewardscitter
{
//
object Scitter
{
//
private[scitter] def exec ute(url : String) =
{
val client = new HttpClient()
val method = new GetMethod(url)
methodgetParams()setParameter(HttpMethodParamsRETRY_HANDLER
new DefaultHttpMethodRetryHandler( false))
clientexecuteMethod(method)
(methodgetStatusLine()getStatusCode() methodgetResponseBodyAsString())
}
}
}
注意两点首先我从 execute() 方法返回一个元组其中包含状态码和响应主体这正是让元组成为语言中固有的一部分的一个强大之处因为实际上很容易从一个方法调用返回多个返回值当然在 Java 代码中也可以通过创建包含元组元素的顶级或嵌套类来实现这一点但是这需要一整套专用于这一个方法的代码此外本来也可以返回一个包含 String 键和 Object 值的 Map但是那样就在很大程度上丧失了类型安全性元组并不是一个非常具有变革性的特性它只不过是又一个使 Scala 成为强大语言的优秀特性
由于使用元组我需要使用 Scala 的另一个特色语法将两个结果都捕捉到本地变量中就像下面这个重写后的 Scittertest 那样
清单 这符合 DRY 原则吗?
package comtednewardscitter
{
//
object Scitter
{
/**
* Ping the server to see if its up and running
*
* Twitter docs say:
* test
* Returns the string ok in the requested format with a OK HTTP status code
* URL:
* Formats: xml json
* Method(s): GET
*/
def test : Boolean =
{
val (statusCode statusBody) =
execute(_timelinexml)
statusCode ==
}
}
}
实际上我可以轻松地将 statusBody 全部去掉并用 _ 替代它因为我没有用过第二个参数(test 没有返回 statusBody)但是对于其他调用将需要这个 statusBody所以出于演示的目的我保留了该参数
注意execute() 没有洩露任何与实际 HTTP 通信相关的细节 — 这是 Encapsulation 这样便于以后用其他实现替换 execute()(以后的确要这么做)或者便于通过重用 HttpClient 对象来优化代码而不是每次重新实例化新的对象
接下来注意到 execute() 方法在 Scitter 对象上吗?这意味着我将可以从不同的 Scitter 实例中使用它(至少现在可以这样做如果以后在 execute() 内部执行的操作不允许这样做则另当别论)— 这就是我将 execute() 标记为 private[scitter] 的原因这意味着 comtednewardscitter 包中的所有内容都可以看到它
(顺便说一句如果还没有运行测试的话那么请运行测试确保一切运行良好我将假设我们在讨论代码时您会运行测试所以如果我忘了提醒您并不意味着您也忘记这么做)
顺便说一句对于经过验证的访问为了支持 Scitter 类需要一个用户名和密码所以我将创建一个重载的 execute() 方法该方法新增两个 String 参数
清单 更加 DRY 化的版本
package comtednewardscitter
{
//
object Scitter
{
//
private[scitter] def execute(url : String username : String password : String) =
{
val client = new HttpClient()
val method = new GetMethod(url)
methodgetParams()setParameter(HttpMethodParamsRETRY_HANDLER
new DefaultHttpMethodRetryHandler( false))
clientgetParams()setAuthenticationPreemptive(true)
clientgetState()setCredentials(
new AuthScope( AuthScopeANY_REALM)
new UsernamePasswordCredentials(username password))
clientexecuteMethod(method)
(methodgetStatusLine()getStatusCode() methodgetResponseBodyAsString())
}
}
}
实际上除了验证部分这两个 execute() 基本上是做相同的事情我们可以按照第二个版本完全重写第一个 execute()但是要注意Scala 要求显式表明重载的 execute() 的返回类型
清单 放弃 DRY
package comtednewardscitter
{
//
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)
methodgetParams()setParameter(HttpMethodParamsRETRY_HANDLER
new DefaultHttpMethodRetryHandler( false))
if ((username != ) && (password != ))
{
clientgetParams()setAuthenticationPreemptive(true)
clientgetState()setCredentials(
new AuthScope( AuthScopeANY_REALM)
new UsernamePasswordCredentials(username password))
}
clientexecuteMethod(method)
(methodgetStatusLine()getStatusCode() methodgetResponseBodyAsString())
}
}
}
到目前为止一切良好我们对 Scitter 的通信部分进行了 DRY 化处理接下来我们转移到下一件事情获得朋友的 tweet 的列表
连接(到朋友)
Twitter API 表明friends_timeline API 调用 返回认证用户和该用户的朋友发表的最近 条状态(它还指出对于直接从 Twitter Web 站点使用 Twitter 的用户这相当于 Web 上的 /home)对于任何 Twitter API 来说这是非常基本的要求所以让我们将它添加到 Scitter 类中之所以将它添加到类而不是对象中是因为正如文档中指出的那样这是以验证用户的身份做的事情而我已经决定归入 Scitter 类而不是 Scitter 对象
但是这里我们碰到一块绊脚石friends_timeline 调用接受一组 可选参数包括 since_idmax_idcount 和 page以控制返回的结果这是一项比较复杂的操作因为 Scala 不像其他语言(例如 GroovyJRuby 或 JavaScript)那样原生地支持 可选参数 的思想但是我们首先来处理简单的东西 — 我们来创建一个 friendsTimeline 方法该方法只执行一般的非参数化的调用
清单 告诉我你身边的朋友是怎样的……
package comtednewardscitter
{
class Scitter
{
def friendsTimeline : List[Status] =
{
val (statusCode statusBody) =
Scitterexecute(_timelinexml
username password)
if (statusCode == )
{
val responseXML = XMLloadString(statusBody)
val statusListBuffer = new ListBuffer[Status]
for (n < (responseXML \\ status)elements)
statusListBuffer += (StatusfromXml(n))
statusListBuffertoList
}
else
{
Nil
}
}
}
}
到目前为止一切良好用于测试的相应方法看上去如下所示
清单 我能判断您是怎样的人 (Miguel de Cervantes)
package comtednewardscittertest
{
class ScitterTests
{
//
@Test def scitterFriendsTimeline =
{
val scitter = new Scitter(testUser testPassword)
val result = scitterfriendsTimeline
assertTrue(resultlength > )
}
}
}
好极了看上去就像 Scitter 对象中的 publicTimeline() 方法并且行为也几乎完全相同
对于我们来说那些可选参数仍然有问题因为 Scala 并没有可选参数这样的语言特性乍一看来惟一的选择就是完整地创建重载的 friendsTimeline() 方法让该方法带有一定数量的参数
幸运的是还有一种更好的方式即通过一种有趣的方式将 Scala 的两个语言特性(有一个特性我还没有提到过) — case 类和 重复参数 结合起来(见清单 )
清单 我有多爱你?……
package comtednewardscitter
{
//
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(_timelinexml?)
for (option < options)
{
option match
{
case Since(since_id) =>
optionsStrappend(since_id= + since_idtoString() + &)
case Max(max_id) =>
optionsStrappend(max_id= + max_idtoString() + &)
case Count(count) =>
optionsStrappend(count= + counttoString() + &)
case Page(page) =>
optionsStrappend(page= + pagetoString() + &)
}
}
val (statusCode statusBody) =
Scitterexecute(optionsStrtoString() username password)
if (statusCode == )
{
val responseXML = XMLloadString(statusBody)
val statusListBuffer = new ListBuffer[Status]
for (n < (responseXML \\ status)elements)
statusListBuffer += (StatusfromXml(n))
statusListBuffertoList
}
else
{
Nil
}
}
}
}
看到标在选项参数后面的 * 吗?这表明该参数实际上是一个参数序列这类似于 Java 中的 varargs 结构和 varargs 一样传递的参数数量可以像前面那样为 (不过我们将需要回到测试代码向 friendsTimeline 调用增加一对括号否则编译器无法作出判断是调用不带参数的方法还是出于部分应用之类的目的而调用该方法)我们还可以开始传递那些 case 类型如下面的清单所示
清单 ……听我细细说(William Shakespeare)
package comtednewardscittertest
{
class ScitterTests
{
//
@Test def scitterFriendsTimelineWithCount =
{
val scitter = new Scitter(testUser testPassword)
val result = scitterfriendsTimeline(Count())
assertTrue(resultlength == )
}
}
}
当然总是存在这样的可能性客户机传入古怪的参数序列例如 friendsTimeline(Count() Count() Count())但是在这里我们只是将列表传递给 Twitter(希望它们的错误处理足够强大能够只采用指定的最后那个参数)当然如果真的担心这一点也很容易在构造发送到 Twitter 的 URL 之前从头至尾检查重复参数列表并采用指定的每种参数的最后一个参数不过后果自负
兼容性
但是这又产生一个有趣的问题从 Java 代码调用这个方法有多容易?毕竟如果这个库的主要目标之一是维护与 Java 代码的兼容性那么我们需要确保 Java 代码在使用它时不至于太麻烦
我们首先通过我们的好朋友 javap 检验一下 Scitter 类
清单 哦没错Java 代码……我现在想起来了……
C:\>javap classpath classes comtednewardscitterScitter
Compiled from scitterscala
public class comtednewardscitterScitter extends javalangObject implements s
calaScalaObject{
public comtednewardscitterScitter(javalangString javalangString);
public scalaList friendsTimeline(scalaSeq);
public boolean verifyCredentials();
public int $tag() throws javarmiRemoteException;
}
这时我心中有两点担心首先friendsTimeline() 带有一个 scalaSeq 参数(这是我们刚才用过的重复参数特性)其次friendsTimeline() 方法和 Scitter 对象中的 publicTimeline() 方法一样(如果不信可以运行 javap 查证)返回一个元素列表 scalaList这两种类型在 Java 代码中有多好用?
最简单的方法是用 Java 代码而不是 Scala 编写一组小型的 JUnit 测试所以接下来我们就这样做虽然可以测试 Scitter 实例的构造并调用它的 verifyCredentials() 方法但这些并不是特别有用 — 记住我们不是要验证 Scitter 类的正确性而是要看看从 Java 代码中使用它有多容易为此我们直接编写一个测试该测试将获取 friends timeline — 换句话说我们要实例化一个 Scitter 实例并且不使用任何参数来调用它的friendsTimeline() 方法
这有点复杂因为需要传入scalaSeq 参数 — scalaSeq 是一个 Scala 特性它将映射到底层 JVM 中的一个接口所以不能直接实例化我们可以尝试典型的 Java null 参数但是这样做会在运行时抛出异常我们需要的是一个 scalaSeq 类以便从 Java 代码中轻松地实例化这个类
最终我们还是在 mutableListBuffer 类型中找到一个这样的类这正是在 Scitter 实现本身中使用的类型
清单 现在我明白了自己为什么喜欢 Scala……
package comtednewardscittertest;
import orgjunit*;
import comtednewardscitter*;
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 (scitterverifyCredentials())
{
scalaList statuses =
scitterfriendsTimeline(new llectionmutableListBuffer());
AssertassertTrue(statuseslength() > );
}
else
AssertassertTrue(false);
}
}
使用返回的 scalaList 不是问题因为我们可以像对待其他 Collection 类一样对待它(不过我们的确怀念 Collection 的一些优点因为 List 上基于 Scala 的方法都假设您将从 Scala 中与它们交互)所以遍历结果并不难只要用上一点 旧式 Java 代码(大约 年时候的风格)
清单 重回 又见 Vector……
package comtednewardscittertest;
import orgjunit*;
import comtednewardscitter*;
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 (scitterverifyCredentials())
{
scalaList statuses =
scitterfriendsTimeline(new llectionmutableListBuffer());
AssertassertTrue(statuseslength() > );
for (int i=; i<statuseslength(); i++)
{
Status stat = (Status)statusesapply(i);
Systemoutprintln(statuser()screenName() + said + stattext());
}
}
else
AssertassertTrue(false);
}
}
这将我们引向另一个部分即将参数传递到 friendsTimeline() 方法不幸的是ListBuffer 类型不是将一个集合作为构造函数参数所以我们必须构造参数列表然后将集合传递到方法调用这样有些单调乏味但还可以承受
清单 现在可以回到 Scala 吗?
package comtednewardscittertest;
import orgjunit*;
import comtednewardscitter*;
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 (scitterverifyCredentials())
{
llectionmutableListBuffer params =
new llectionmutableListBuffer();
params$plus$eq(new Count());
scalaList statuses = scitterfriendsTimeline(params);
AssertassertTrue(statuseslength() > );
AssertassertTrue(statuseslength() == );
for (int i=; i<statuseslength(); i++)
{
Status stat = (Status)statusesapply(i);
Systemoutprintln(statuser()screenName() + said + stattext());
}
}
else
AssertassertTrue(false);
}
}
所以虽然 Java 版本比对应的 Scala 版本要冗长一点但是到目前为止从任何要使用 Scitter 库的 Java 客户机中调用该库仍然非常简单好极了
结束语
显然对于 Scitter 还有很多事情要做但是它已经逐渐丰满起来感觉不错我们设法对 Scitter 库的通信部分进行了 DRY 化处理并且为 Twitter 提供的很多不同的 API 调用合并了所需的可选参数 — 到目前为止Java 客户机基本上没有受到我们公布的 API 的拖累即使 API 没有 Scala 所使用的那些 API 那样干净但是如果 Java 开发人员要使用 Scitter 库也不需要费太多力气
Scitter 库仍然带有对象的意味不过我们也开始看到一些有实用意义的 Scala 特性正在出现随着我们继续构建这个库只要有助于使代码更简洁更清晰越来越多这样的特性将添加进来本应如此
是时候说再见了我要短暂离开一下等我回来时我将为这个库增加对离线测试的支持并增加更新用户状态的功能到那时Scala 迷们请记住功能正常总比功能失常好(对不起我只是太喜欢开玩笑了)