为何选择Scala
Scala的亮点
简洁代码
Scala提供的脚本特性以及将函数作为一等公民的方式,使得它可以去掉不少在Java中显得冗余的代码,例如不必要的类定义,不必要的main函数声明。Scala提供的类型推断机制,也使得代码精简成为可能。Scala还有一个巧妙的设计,就是允许在定义类的同时定义该类的主构造函数。在大多数情况下,可以避免我们声明不必要的构造函数。
Scala还提供了一些非常有用的语法糖,如伴生对象,样例类,既简化了接口,也简化了我们需要书写的代码。例如如下代码:
case class Person(name: String, age: Int)
val l = List(Person("Jack", 28), Person("Bruce", 30))
这里的List和Person都提供了伴生对象,避免再写冗余的new。这种方式对于DSL支持也是有帮助的。Person是一个样例类,虽然只有这么一行代码,蕴含的含义却非常丰富——它为Person提供了属性,属性对应的访问器,equals和hashcode方法,伴生对象,以及对模式匹配的支持。在Scala 2.11版本中,还突破了样例类属性个数的约束。由于样例类是不变的,也能实现trait,因而通常作为message而被广泛应用到系统中。例如在AKKA中,actor之间传递的消息都应该尽量定义为样例类。
支持OO与FP
将面向对象与函数式编程有机地结合,本身就是Martin Odersky以及Scala的目标。这二者的是非,我从来不予以置评。个人认为应针对不同场景,选择不同的设计思想。基于这样的思想,Scala成为我的所爱,也就是顺其自然的事情了。
演讲中,我主要提及了纯函数的定义,并介绍了应该如何设计没有副作用的纯函数。纯函数针对给定的输入,总是返回相同的输出,且没有任何副作用,就使得纯函数更容易推论(这意味着它更容易测试),更容易组合。从某种角度来讲,这样的设计指导思想与OO阵营中的CQS原则非常一致,只是重用的粒度不一样罢了。
我给出了Functional Programming in Scala一书中的例子。如下代码中的declareWinner函数并非纯函数:
object Game {
def printWinner(p: Player): Unit =
println(p.name + " is the winner!")
def declareWinner(p1: Player, p2: Player): Unit =
if (p1.score > p2.score)
printWinner(p1)
else printWinner(p2)
}
这里的printWinner要向控制台输出字符串,从而产生了副作用。(简单的判断标准是看函数的返回值是否为Unit)我们需要分离出专门返回winner的函数:
def winner(p1: Player, p2: Player): Player =
if (p1.score > p2.score) p1 else p2
消除了副作用,函数的职责变得单一,我们就很容易对函数进行组合或重用了。除了可以打印winner之外,例如我们可以像下面的代码那样获得List中最终的获胜者:
val players = List(Player("Sue", 7), Player("Bob", 8), Player("Joe", 4))
val finalWinner = players.reduceLeft(winner)
函数的抽象有时候需要脑洞大开,需要敏锐地去发现变化点与不变点,然后提炼出函数。例如,当我们定义了这样的List之后,比较sum与product的异同:
sealed trait MyList[+T]
case object Nil extends MyList[Nothing]
case class Cons[+T](h: T, t: MyList[T]) extends MyList[T]
object MyList {
def sum(ints: MyList[Int]):Int = ints match {
case Nil => 0
case Cons(h, t) => h + sum(t)
}
def product(ds: MyList[Double]):Double = ds match {
case Nil => 1.0
case Cons(h, t) => h * product(t)
}
def apply[T](xs: T*):MyList[T] =
if (xs.isEmpty) Nil
else Cons(xs.head, apply(xs.tail: _*))
}
sum与product的相同之处都是针对List的元素进行运算,运算规律是计算两个元素,将结果与第三个元素进行计算,然后依次类推。这就是在函数式领域中非常常见的折叠(fold)计算:
def foldRight[A, B](l: MyList[A], z: B)(f: (A, B) => B):B = l match {
case Nil => z
case Cons(x, xs) => f(x, foldRight(xs, z)(f))
}
在引入了foldRight函数后,sum和product就可以重用foldRight了:
def sum(ints: MyList[Int]):Int = foldRight(ints, 0)(_ + _)
def product(ds: MyList[Double]):Double = foldRight(ds, 0.0)(_ * _)
在函数式编程的世界里,事实上大多数数据操作都可以抽象为filter,map,fold以及flatten几个操作。查看Scala的集合库,可以验证这个观点。虽然Scala集合提供了非常丰富的接口,但其实现基本上没有超出这四个操作的范围。
高阶函数
虽然Java 8引入了简洁的Lambda表达式,使得我们终于脱离了冗长而又多重嵌套的匿名类之苦,但就其本质,它实则还是接口,未能实现高阶函数,即未将函数视为一等公民,无法将函数作为方法参数或返回值。例如,在Java中,当我们需要定义一个能够接收lambda表达式的方法时,还需要声明形参为接口类型,Scala则省去了这个步骤:
def find(predicate: Person => Boolean)
结合Curry化,还可以对函数玩出如下的魔法:
def add(x: Int)(y: Int) = x + y
val addFor = add(2) _
val result = addFor(5)
表达式add(2) _返回的事实上是需要接受一个参数的函数,因此addFor变量的类型为函数。此时result的结果为7。
当然,从底层实现来看,Scala中的所有函数其实仍然是接口类型,可以说这种高阶函数仍然是语法糖。Scala之所以能让高阶函数显得如此自然,还在于它自己提供了基于JVM的编译器。
丰富的集合操作
虽然集合的多数操作都可以视为对foreach, filter, map, fold等操作的封装,但一个具有丰富API的集合库,却可以让开发人员更加高效。例如Twitter给出了如下的案例,要求从一组投票结果(语言,票数)中统计不同程序语言的票数并按照得票的顺序显示:
val votes = Seq(("scala", 1), ("java", 4), ("scala", 10), ("scala", 1), ("python", 10))
val orderedVotes = votes
.groupBy(_._1)
.map { case (which, counts) =>
(which, counts.foldLeft(0)(_ + _._2))
}.toSeq
.sortBy(_._2)
.reverse
这段代码首先将Seq按照语言类别进行分组。分组后得到一个Map[String, Seq[(Stirng, Int)]]类型:
scala.collection.immutable.Map[String,Seq[(String, Int)]] = Map(scala -> List((scala,1), (scala,10), (scala,1)), java -> List((java,4)), python -> List((python,10)))
然后将这个类型转换为一个Map。转换时,通过foldLeft操作对前面List中tuple的Int值累加,所以得到的结果为:
scala.collection.immutable.Map[String,Int] = Map(scala -> 12, java -> 4, python -> 10)
之后,将Map转换为Seq,然后按照统计的数值降序排列,接着反转顺序即可。
显然,这些操作非常适用于数据处理场景。事实上,Spark的RDD也可以视为一种集合,提供了比Scala更加丰富的操作。此外,当我们需要编写这样的代码时,还可以在Scala提供的交互窗口下对算法进行spike,这是目前的Java所不具备的。
Stream
Stream与大数据集合操作的性能有关。由于函数式编程对不变性的要求,当我们操作集合时,都会产生一个新的集合,当集合元素较多时,会导致大量内存的消耗。例如如下的代码,除原来的集合外,还另外产生了三个临时的集合:
List(1,2,3,4).map (_ + 10).filter (_ % 2 == 0).map (_ * 3)
比较对集合的while操作,这是函数式操作的缺陷。虽可换以while来遍历集合,却又丢失了函数的高阶组合(high-level compositon)优势。
解决之道就是采用non-strictness的集合。在Scala中,就是使用stream。关于这部分内容,我的同事崔鹏飞已有文章《Scala中Stream的应用场景及其实现原理》作了详细叙述。
并发与并行
Scala本身属于JVM语言,因此仍然支持Java的并发处理方式。若我们能遵循函数式编程思想,则建议有效运用Scala支持的并发特性。由于Scala在2.10版本中将原有的Actor取消,转而使用AKKA,所以我在演讲中并没有提及Actor。这是另外一个大的话题。
除了Actor,Scala中值得重视的并发特性就是Future与Promise。默认情况下,future和promise都是非阻塞的,通过提供回调的方式获得执行的结果。future提供了onComplete、onSuccess、onFailure回调。如下代码:
println("starting calculation ...")
val f = Future {
sleep(Random.nextInt(500))
42
}
println("before onComplete")
f.onComplete {
case Success(value) => println(s"Got the callback, meaning = $value")
case Failure(e) => e.printStackTrace
}
// do the rest of your work
println("A ..."); sleep(100)
println("B ..."); sleep(100)
println("C ..."); sleep(100)
println("D ..."); sleep(100)
println("E ..."); sleep(100)
println("F ..."); sleep(100)
sleep(2000)
f的执行结果可能会在打印A到F的任何一个时间触发onComplete回调,以打印返回的结果。注意,这里的f是Future对象。
我们还可以利用for表达式组合多个future,AKKA中的ask模式也经常采用这种方式:
object Cloud {
def runAlgorithm(times: Int): Future[Int] = Future {
Thread.sleep(times)
times
}
}
object CloudApp extends App {
val result1 = Cloud.runAlgorithm(10) //假设runAlgorithm需要耗费较长时间
val result2 = Cloud.runAlgorithm(20)
val result3 = Cloud.runAlgorithm(30)
val result = for {
r1 <- result1
r2 <- result2
r3 <- result3
} yield (r1 + r2 + r3)
result onSuccess {
case result => println(s"total = $result")
}
Thread.sleep(2000)
}
这个例子会并行的执行三个操作,最终需要的时间取决于耗时最长的操作。注意,yield返回的仍然是一个future对象,它持有三个future结果的和。
promise相当于是future的工厂,只是比单纯地创建future具有更强的功能。这里不再详细介绍。
Scala提供了非常丰富的并行集合,它的核心抽象是splitter与combiner,前者负责分解,后者就像builder那样将拆分的集合再进行合并。在Scala中,几乎每个集合都对应定义了并行集合。多数情况下,可以调用集合的par方法来创建。
例如,我们需要抓取两个网站的内容并显示:
val urls = List("http://scala-lang.org",
"http://agiledon.github.com")
def fromURL(url: String) = scala.io.Source.fromURL(url).getLines().mkString("\n")
val t = System.currentTimeMillis()
urls.par.map(fromURL(_))
println
println("time: " + (System.currentTimeMillis - t) + "ms")
如果没有添加par方法,程序就会顺序抓取两个网站内容,效率差不多会低一半。
那么,什么时候需要将集合转换为并行集合呢?这当然取决于集合大小。但这并没有所谓的标准值。因为影响执行效率的因素有很多,包括CPU的类型、核数、JVM的版本、集合元素的workload、特定操作、以及内存管理等。
并行集合会启动多个线程来执行,默认情况下,会根据cpu核数以及jvm的设置来确定。如果有兴趣,可以选择两台cpu核数不同的机器分别运行如下代码:
(1 to 10000).par.map(i => Thread.currentThread.getName).distinct.size
这段代码可以获得线程的数量。
我在演讲时,有人提问这种线程数量的灵活判断究竟取决于编译的机器,还是运行的机器?答案是和运行的机器有关。这事实上是由JVM的编译原理决定的。JVM的编译与纯粹的静态编译不同,Java和Scala编译器都是将源代码转换为JVM字节码,而在运行时,JVM会根据当前运行机器的硬件架构,将JVM字节码转换为机器码。这就是所谓的JIT(just-in-time)编译。
Scala还有很多优势,包括模式匹配、隐式转换、类型类、更好的泛型协变逆变等,当然这些特性也是造成Scala变得更复杂的起因。我们需要明智地判断,控制自己卖弄技巧的欲望,在代码可读性与高效精简之间取得合理的平衡。
题外话
说些题外话,当我推荐Scala时,提出质疑最多的往往不是Java程序员,而是负责团队的管理者,尤其是略懂技术或者曾经做过技术的管理者。他们会表示这样那样的担心,例如Scala的编译速度慢,调试困难,学习曲线高,诸如此类。
编译速度一直是Scala之殇,由于它相当于做了两次翻译,且需要对代码做一些优化,这个问题一时很难彻底根治。
调试困难被吐槽得较激烈,这是因为Scala的调试信息总是让人难以定位。虽然在2.9之后,似乎已有不少改进,但由于类型推断等特性的缘故,相较Java而言,打印的栈信息仍有词不达意之处。曲线救国的方式是多编写小的、职责单一的类(尤其是trait),尽量编写纯函数,以及提高测试覆盖率。此外,调试是否困难还与开发者自身对于Scala这门语言的熟悉程度有关,不能将罪过一味推诿给语言本身。
至于学习曲线高的问题,其实还在于我们对Scala的定位,即确定我们是开发应用还是开发库。此外,对于Scala提供的一些相对晦涩难用的语法,我们尽可以不用。ThoughtWorks技术雷达上将“Scala, the good parts”放到Adopt,而非整个Scala,寓意意味深长。
通常而言,OO转FP会显得相对困难,这是两种根本不同的思维范式。张无忌学太极剑时,学会的是忘记,只取其神,我们学FP,还得尝试忘记OO。自然,学到后来,其实还是万法归一。OO与FP仍然有许多相同的设计原则,例如单一职责,例如分而治之。
对于管理者而言,最关键的一点是明白Scala与Java的优劣对比,然后根据项目情况和团队情况,明智地进行技术决策。我们不能完全脱离上下文去说A优于B。世上哪有绝对呢?
日渐成熟的Scala技术栈
Scala社区的发展
然而,一门语言并不能孤立地存在,必须提供依附的平台,以及围绕它建立的生态圈。不如此,语言则不足以壮大。Ruby很优秀,但如果没有Ruby On Rails的推动,也很难发展到今天这个地步。Scala同样如此。反过来,当我们在使用一门语言时,也要选择符合这门语言的技术栈,在整个生态圈中找到适合具体场景的框架或工具。
当然,我们在使用Scala进行软件开发时,亦可以寻求庞大的Java社区支持;可是,如果选择调用Java开发的库,就会牺牲掉Scala给我们带来的福利。幸运的是,在如今,多数情况你已不必如此。伴随着Scala语言逐渐形成的Scala社区,已经开始慢慢形成相对完整的Scala技术栈。无论是企业开发、自动化测试或者大数据领域,这些框架或工具已经非常完整地呈现了Scala开发的生态系统。
快速了解Scala技术栈
若要了解Scala技术栈,并快速学习这些框架,一个好的方法是下载typesafe推出的Activator。它提供了相对富足的基于Scala以及Scala主流框架的开发模板,这其中实则还隐含了typesafe为Scala开发提供的最佳实践与指导。下图是Activator模板的截图:
那么,是否有渠道可以整体地获知Scala技术栈到底包括哪些框架或工具,以及它们的特性与使用场景呢?感谢Lauris Dzilums以及其他在Github的Contributors。在Lauris Dzilums的Github上,他建立了名为awesome-scala的Repository,搜罗了当下主要的基于Scala开发的框架与工具,涉及到的领域包括:
- Database
- Web Frameworks
- i18n
- Authentication
- Testing
- JSON Manipulation
- Serialization
- Science and Data Analysis
- Big Data
- Functional Reactive Programming
- Modularization and Dependency Injection
- Distributed Systems
- Extensions
- Android
- HTTP
- Semantic Web
- Metrics and Monitoring
- Sbt plugins
是否有“乱花渐欲迷人眼”的感觉?不是太少,而是太多!那就让我删繁就简,就我的经验介绍一些框架或工具,从持久化、分布式系统、HTTP、Web框架、大数据、测试这六方面入手,作一次蜻蜓点水般的俯瞰。
持久化
归根结底,对数据的持久化主要还是通过JDBC访问数据库。但是,我们需要更好的API接口,能更好地与Scala契合,又或者更自然的ORM。如果希望执行SQL语句来操作数据库,那么运用相对广泛的是框架ScalikeJDBC,它提供了非常简单的API接口,甚至提供了SQL的DSL语法。例如:
val alice: Option[Member] = withSQL {
select.from(Member as m).where.eq(m.name, name)
}.map(rs => Member(rs)).single.apply()
如果希望使用ORM框架,Squeryl应该是很好的选择。该框架目前的版本为0.9.5,已经比较成熟了。Squeryl支持按惯例映射对象与关系表,相当于定义一个POSO(Plain Old Scala Object),从而减少框架的侵入。若映射违背了惯例,则可以利用框架定义的annotation如@Column定义映射。框架提供了org.squeryl.Table[T]来完成这种映射关系。
因为可以运用Scala的高阶函数、偏函数等特性,使得Squeryl的语法非常自然,例如根据条件对表进行更新:
update(songs)(s =>
where(s.title === "Watermelon Man")
set(s.title := "The Watermelon Man",
s.year := s.year.~ + 1)
)
分布式系统
我放弃介绍诸如模块化管理以及依赖注入,是因为它们在Scala社区的价值不如Java社区大。例如,我们可以灵活地运用trait结合cake pattern就可以实现依赖注入的特性。因此,我直接跳过这些内容,来介绍影响更大的支持分布式系统的框架。
Finagle的血统高贵,来自过去的寒门,现在的高门大族Twitter。Twitter是较早使用Scala作为服务端开发的互联网公司,因而积累了非常多的Scala经验,并基于这些经验推出了一些颇有影响力的框架。由于Twitter对可伸缩性、性能、并发的高要求,这些框架也极为关注这些质量属性。Finagle就是其中之一。它是一个扩展的RPC系统,以支持高并发服务器的搭建。我并没有真正在项目中使用过Finagle,大家可以到它的官方网站获得更多消息。
对于分布式的支持,绝对绕不开的框架还是AKKA。它产生的影响力如此之大,甚至使得Scala语言从2.10开始,就放弃了自己的Actor模型,转而将AKKA Actor收编为2.10版本的语言特性。许多框架在分布式处理方面也选择了使用AKKA,例如Spark、Spray。AKKA的Actor模型参考了Erlang语言,为每个Actor提供了一个专有的Mailbox,并将消息处理的实现细节做了良好的封装,使得并发编程变得更加容易。AKKA很好地统一了本地Actor与远程Actor,提供了几乎一致的API接口。AKKA也能够很好地支持消息的容错,除了提供一套完整的Monitoring机制外,还提供了对Dead Letter的处理。
AKKA天生支持EDA(Event-Driven Architecture)。当我们针对领域建模时,可以考虑针对事件进行建模。在AKKA中,这些事件模型可以被定义为Scala的case class,并作为消息传递给Actor。借用Vaughn Vernon在《实现领域驱动设计》中的例子,针对如下的事件流:
我们可以利用Akka简单地实现:
case class AllPhoneNumberListed(phoneNumbers: List[Int])
case class PhoneNumberMatched(phoneNumbers: List[Int])
case class AllPhoneNumberRead(fileName: String)
class PhoneNumbersPublisher(actor: ActorRef) extends ActorRef {
def receive = {
case ReadPhoneNumbers =>
//list phone numbers
actor ! AllPhoneNumberListed(List(1110, ))
}
}
class PhoneNumberFinder(actor: ActorRef) extends ActorRef {
def receive = {
case AllPhoneNumberListed(numbers) =>
//match
actor ! PhoneNumberMatched()
}
}
val finder = system.actorOf(Prop(new PhoneNumberFinder(...)))
val publisher = system.actorOf(Prop(new PhoneNumbersPublisher(finder)))
publisher ! ReadPhoneNumbers("callinfo.txt")
若需要处理的电话号码数据量大,我们可以很容易地将诸如PhoneNumbersPublisher、PhoneNumberFinder等Actors部署为Remote Actor。此时,仅仅需要更改客户端获得Actor的方式即可。
Twitter实现的Finagle是针对RPC通信,Akka则提供了内部的消息队列(MailBox),而由LinkedIn主持开发的Kafka则提供了支持高吞吐量的分布式消息队列中间件。这个顶着文学家帽子的消息队列,能够支持高效的Publisher-Subscriber模式进行消息处理,并以快速、稳定、可伸缩的特性很快引起了开发者的关注,并在一些框架中被列入候选的消息队列而提供支持,例如,Spark Streaming就支持Kafka作为流数据的Input Source。
HTTP
严格意义上讲,Spray并非单纯的HTTP框架,它还支持REST、JSON、Caching、Routing、IO等功能。Spray的模块及其之间的关系如下图所示:
我在项目中主要将Spray作为REST框架来使用,并结合AKKA来处理领域逻辑。Spray处理HTTP请求的架构如下图所示:
Spray提供了一套DSL风格的path语法,能够非常容易地编写支持各种HTTP动词的请求,例如:
trait HttpServiceBase extends Directives with Json4sSupport {
implicit val system: ActorSystem
implicit def json4sFormats: Formats = DefaultFormats
def route: Route
}
trait CustomerService extends HttpServiceBase {
val route =
path("customer" / "groups") {
get {
parameters('groupids.?) {
(groupids) =>
complete {
groupids match {
case Some(groupIds) =>
ViewUserGroup.queryUserGroup(groupIds.split(",").toList)
case None => ViewUserGroup.queryUserGroup()
}
}
}
}
} ~
path("customers" / "vip" / "failureinfo") {
post {
entity(as[FailureVipCustomerRequest]) {
request =>
complete {
VipCustomer.failureInfo(request)
}
}
}
}
}
我个人认为,在进行Web开发时,完全可以放弃Web框架,直接选择AngularJS结合Spray和AKKA,同样能够很好地满足Web开发需要。
Spray支持REST,且Spray自身提供了服务容器spray-can,因而允许Standalone的部署(当然也支持部署到Jetty和tomcat等应用服务器)。Spray对HTTP请求的内部处理机制实则是基于Akka-IO,通过IO这个Actor发出对HTTP的bind消息。例如:
IO(Http) ! Http.Bind(service, interface = "0.0.0.0", port = 8889)
我们可以编写不同的Boot对象去绑定不同的主机Host以及端口。这些特性都使得Spray能够很好地支持当下较为流行的Micro Service架构风格。
Web框架
正如前面所说,当我们选择Spray作为REST框架时,完全可以选择诸如AngularJS或者Backbone之类的JavaScript框架开发Web客户端。客户端能够处理自己的逻辑,然后再以JSON格式发送请求给REST服务端。这时,我们将模型视为资源(Resource),视图完全在客户端。JS的控制器负责控制客户端的界面逻辑,服务端的控制器则负责处理业务逻辑,于是传统的MVC就变化为VC+R+C模式。这里的R指的是Resource,而服务端与客户端则通过JSON格式的Resource进行通信。
若硬要使用专有的Web框架,在Scala技术栈下,最为流行的就是Play Framework,这是一个标准的MVC框架。另外一个相对小众的Web框架是Lift。它与大多数Web框架如RoR、Struts、Django以及Spring MVC、Play不同,采用的并非MVC模式,而是使用了所谓的View First。它驱动开发者对内容生成与内容展现(Markup)形成“关注点分离”。
Lift将关注点重点放在View上,这是因为在一些Web应用中,可能存在多个页面对同一种Model的Action。倘若采用MVC中的Controller,会使得控制变得非常复杂。Lift提出了一种所谓view-snippet-model(简称为VSM)的模式。
View主要为响应页面请求的HTML内容,分为template views和generated views。Snippet的职责则用于生成动态内容,并在模型发生更改时,对Model和View进行协调。
大数据
大数据框架最耀眼的新星非Spark莫属。与许多专有的大数据处理平台不同,Spark建立在统一抽象的RDD之上,使得它可以以基本一致的方式应对不同的大数据处理场景,包括MapReduce,Streaming,SQL,Machine Learning以及Graph等。这即Matei Zaharia所谓的“设计一个通用的编程抽象(Unified Programming Abstraction)。
由于Spark具有先进的DAG执行引擎,支持cyclic data flow和内存计算。因此相比较Hadoop而言,性能更优。在内存中它的运行速度是Hadoop MapReduce的100倍,在磁盘中是10倍。
由于使用了Scala语言,通过高效利用Scala的语言特性,使得Spark的总代码量出奇地少,性能却在多数方面都具备一定的优势(只有在Streaming方面,逊色于Storm)。下图是针对Spark 0.9版本的BenchMark:
由于使用了Scala,使得语言的函数式特性得到了最棒的利用。事实上,函数式语言的诸多特性包括不变性、无副作用、组合子等,天生与数据处理匹配。于是,针对WordCount,我们可以如此简易地实现:
file = spark.textFile("hdfs://...")
file.flatMap(line => line.split(" "))
.map(word => (word, 1))
.reduceByKey(_ + _)
要是使用Hadoop,就没有这么方便了。幸运的是,Twitter的一个开源框架scalding提供了对Hadoop MapReduce的抽象与包装。它使得我们可以按照Scala的方式执行MapReduce的Job:
class WordCountJob(args : Args) extends Job(args) {
TextLine( args("input") )
.flatMap('line -> 'word) { line : String => tokenize(line) }
.groupBy('word) { _.size }
.write( Tsv( args("output") ) )
// Split a piece of text into individual words.
def tokenize(text : String) : Array[String] = {
// Lowercase each word and remove punctuation.
text.toLowerCase.replaceAll("[^a-zA-Z0-9\\s]", "").split("\\s+")
}
}
测试
虽然我们可以使用诸如JUnit、TestNG为Scala项目开发编写单元测试,使用Cocumber之类的BDD框架编写验收测试。但在多数情况下,我们更倾向于选择使用ScalaTest或者Specs2。在一些Java开发项目中,我们也开始尝试使用ScalaTest来编写验收测试,乃至于单元测试。
若要我选择ScalaTest或Specs2,我更倾向于ScalaTest,这是因为ScalaTest支持的风格更具备多样性,可以满足各种不同的需求,例如传统的JUnit风格、函数式风格以及Spec方式。我的一篇博客《ScalaTest的测试风格》详细介绍了各自的语法。
一个被广泛使用的测试工具是Gatling,它是基于Scala、AKKA以及Netty开发的性能测试与压力测试工具。我的同事刘冉在InfoQ发表的文章《新一代服务器性能测试工具Gatling》对Gatling进行了详细深入的介绍。
ScalaMeter也是一款很不错的性能测试工具。我们可以像编写ScalaTest测试那样的风格来编写ScalaMeter性能测试用例,并能够快捷地生成性能测试数据。这些功能都非常有助于我们针对代码或软件产品进行BenchMark测试。我们曾经用ScalaMeter来编写针对Scala集合的性能测试,例如比较Vector、ArrayBuffer、ListBuffer以及List等集合的相关操作,以便于我们更好地使用Scala集合。以下代码展示了如何使用ScalaMeter编写性能测试:
import org.scalameter.api._
object RangeBenchmark
extends PerformanceTest.Microbenchmark {
val ranges = for {
size <- Gen.range("size")(300000, 1500000, 300000)
} yield 0 until size
measure method "map" in {
using(ranges) curve("Range") in {
_.map(_ + 1)
}
}
}
根据场景选择框架或工具
比起Java庞大的社区,以及它提供的浩如烟海般的技术栈,Scala技术栈差不多可以说是沧海一粟。然而,麻雀虽小却五脏俱全,何况Scala以及Scala技术栈仍然走在迈向成熟的道路上。对于Scala程序员而言,因为项目的不同,未必能涉猎所有技术栈,而且针对不同的方面,也有多个选择。在选择这些框架或工具时,应根据实际的场景做出判断。为稳妥起见,最好能运用技术矩阵地方式对多个方案进行设计权衡与决策。
我们也不能固步自封,视Java社区而不顾。毕竟那些Java框架已经经历了千锤百炼,并有许多成功的案例作为佐证。关注Scala技术栈,却又不局限自己的视野,量力而为,选择合适的技术方案,才是设计与开发的正道。