1. 程式人生 > >Scala詳解---------快速入門Scala

Scala詳解---------快速入門Scala

我無可救藥地成為了Scala的超級粉絲。在我使用Scala開發專案以及編寫框架後,它就彷彿凝聚成為一個巨大的黑洞,吸引力使我不得不飛向它,以至於開始背離Java。固然Java 8為Java陣營增添了一絲亮色,卻是望眼欲穿,千呼萬喚始出來。而Scala程式設計師,卻早就在享受lambda、高階函式、trait、隱式轉換等帶來的福利了。

Java像是一頭史前巨獸,它在OO的方向上幾乎走到了極致,硬將它拉入FP陣營,確乎有些強人所難了。而Scala則不,因為它的誕生就是OO與FP的混血兒——完美的基因融合。

“Object-Oriented Meets Functional”,這是Scala語言官方網站上飄揚的旗幟。這也是Scala的野心,當然,也是Martin Odersky的雄心。

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技術棧,卻又不侷限自己的視野,量力而為,選擇合適的技術方案,才是設計與開發的正道。

作者簡介

張逸,現為ThoughtWorks Lead Consultant。作為一名諮詢師,主要為客戶提供組織的敏捷轉型、過程改進、企業系統架構、領域驅動設計、大資料、程式碼質量提升、測試驅動開發等諮詢與培訓工作。

本文借鑑於http://www.infoq.com/cn/articles/scala-technology/