編碼修煉 | 快速瞭解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(https://github.com/lauris/awesome-scala),蒐羅了當下主要的基於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語法。
如果希望使用ORM框架,Squeryl應該是很好的選擇。我的同事楊雲在專案中使用過該框架,體驗不錯。該框架目前的版本為0.9.5,已經比較成熟了。Squeryl支援按慣例對映物件與關係表,相當於定義一個POSO(Plain Old Scala Object),從而減少框架的侵入。若對映違背了慣例,則可以利用框架定義的annotation如@Column定義對映。框架提供了org.squeryl.Table[T]來完成這種對映關係。
分散式系統
我放棄介紹諸如模組化管理以及依賴注入,是因為它們在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的處理。
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動詞的請求。
我個人認為,在進行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的測試風格(http://agiledon.github.io/blog/2014/01/13/testing-styles-of-scalatest/)》詳細介紹了各自的語法。
一個被廣泛使用的測試工具是Gatling,它是基於Scala、AKKA以及Netty開發的效能測試與壓力測試工具。我的同事劉冉在InfoQ發表的文章《新一代伺服器效能測試工具Gatling(http://www.infoq.com/cn/articles/new-generation-server-testing-tool-gatling)》對Gatling進行了詳細深入的介紹。
ScalaMeter也是一款很不錯的效能測試工具。我們可以像編寫ScalaTest測試那樣的風格來編寫ScalaMeter效能測試用例,並能夠快捷地生成效能測試資料。這些功能都非常有助於我們針對程式碼或軟體產品進行BenchMark測試。我們曾經用ScalaMeter來編寫針對Scala集合的效能測試,例如比較Vector、ArrayBuffer、ListBuffer以及List等集合的相關操作,以便於我們更好地使用Scala集合。
根據場景選擇框架或工具
比起Java龐大的社群,以及它提供的浩如煙海般的技術棧,Scala技術棧差不多可以說是滄海一粟。然而,麻雀雖小卻五臟俱全,何況Scala以及Scala技術棧仍然走在邁向成熟的道路上。對於Scala程式設計師而言,因為專案的不同,未必能涉獵所有技術棧,而且針對不同的方面,也有多個選擇。在選擇這些框架或工具時,應根據實際的場景做出判斷。為穩妥起見,最好能運用技術矩陣地方式對多個方案進行設計權衡與決策。
我們也不能固步自封,視Java社群而不顧。畢竟那些Java框架已經經歷了千錘百煉,並有許多成功的案例作為佐證。關注Scala技術棧,卻又不侷限自己的視野,量力而為,選擇合適的技術方案,才是設計與開發的正道。