Scala超程式設計:實現lombok.Data
如果你讀完了《Scala超程式設計:伊甸園初窺》,理論上你已經具備實現lombok.Data
的能力了。
所以,我建議你不要閱讀本文,直接自己嘗試。
定義 lombok.Data 的 Scala 版
@data
class A {
var x: Int = _
var y: String = _
}
複製程式碼
我們希望通過@data
這個註釋,自動生成如下程式碼:
class A {
var x: Int = _
var y: String = _
def getX(): Int = x
def setX(paramX: Int): Unit = { x = paramX }
def getY(): Int = x
def setY(paramY: String): Unit = { y = paramY }
}
複製程式碼
為什麼要生成這樣的程式碼呢?就個人而言,我是為了在Spring Boot和Scala混合編寫的專案中無縫地使用MyBatis。在使用Java時,我們可以很方便地使用lombok.Data生成我們所需的Getter和Setter。而在Scala生態中,已經有了case class,這種寫法其實對於Pure Scala的程式設計師來說,是相當離經叛道的。
在用Scala和Java混合程式設計的時候,我覺得其實最重要的一點是
選擇
。實現一個功能的方式可能有100(二進位制哦)種,但是最適合的方式,永遠只有一種。
我選擇了MyBatis,不採用超程式設計的手段(其實這段時間我剛剛學會),我是這樣做的:
import scala.beans.BeanProperty
class A {
@BeanProperty var x: Int = _
@BeanProperty var y: String = _
}
複製程式碼
用Vim列編輯,其實也還好。但是我內心其實一直在對自己說:DO NOT REPEAT YOURSELF
。
參考實現
// ...
annottees.map(_.tree).toList match {
case q"""
class $name {
..$vars
}
""" :: Nil =>
// Generate the Getter and Setter from VarDefs
val beanMethods = vars.collect {
case q"$mods var $name: $tpt = $expr" =>
val getName = TermName("get" + name.encodedName.toString.capitalize)
val setName = TermName("set" + name.encodedName.toString.capitalize)
println(getName)
val ident = Ident(name)
List (
q"def $getName: $tpt = $ident",
q"def $setName(paramX: $tpt): Unit = { $ident = paramX }"
)
}.flatten
// Insert the generated Getter and Setter
q"""
class $name {
..$vars
..$beanMethods
}
"""
case _ =>
throw new Exception("Macro Error")
}
}
// ...
複製程式碼
單元測試
上一篇超程式設計相關的文章實際上主要是為了強調構建,所以我貼了兩次構建定義的程式碼。
test("generate setter and getter") {
@data
class A {
var x: Int = _
var y: String = _
}
val a = new A
a.setX(12)
assert(a.getX === 12)
a.setY("Hello")
assert(a.getY === "Hello")
}
複製程式碼
lombok在IntelliJ Idea中有專門的外掛,去處理Idea無法定位到的程式自動生成的Getter和Setter。如果我們只是為了讓MyBatis能夠識別和使用,我們就沒有必要再去為我們的Scala版lombok.Data
專門定製一個外掛。在我們自己的程式碼中,沒有必要使用Getter和Setter,因為Scala在語言級別已經支援了(如果你一臉懵逼,我建議你先閱讀一下《快學Scala》和《Scala實用指南》的樣章)。
test("handle operator in the name") {
@data
class B {
var op_+ : Int = _
}
val b = new B
b.setOp_+(42)
assert(b.getOp_+ === 42)
}
複製程式碼
這個地方也涉及到了一個Scala相關的知識點,我記得在《快學Scala》中看到過。在參考實現中,與這個單測相關的程式碼是這兩行:
val getName = TermName("get" + name.encodedName.toString.capitalize)
val setName = TermName("set" + name.encodedName.toString.capitalize)
複製程式碼
這裡就不展開了。
單元測試的風格
Scala專案的單測,我一直用ScalaTest,但是ScalaTest官網的例子給的是FlatSpec:
"A Stack" should "pop values in last-in-first-out order" in {
val stack = new Stack[Int]
stack.push(1)
stack.push(2)
stack.pop() should be (2)
stack.pop() should be (1)
}
複製程式碼
大概是這種程式碼風格。我們需要在兩個地方填入一些資訊,有點煩人。所以,我推薦FunSuite這種風格:
test("A Stack pop values in last-in-first-out order"){
val stack = new Stack[Int]
stack.push(1)
stack.push(2)
stack.pop() should be (2)
stack.pop() should be (1)
}
複製程式碼
我只需要填一句話,不需要考慮主語,對IDE也更加友好。
編譯期與執行時
這是超程式設計裡面兩個特別重要的概念。廣義上講,實際上,這些概念都在試圖提醒我們,注意一下是誰(那臺機器上的那個程序)在執行我們的程式碼。
提交程式碼的時候,不小心忘記把除錯用的println(getName)
清理掉,索性就不去清理了。
使用sbt去執行我們的單元測試:
$ sbt
sbt:paradise-study> test
// 編譯期開始
[info] Compiling 1 Scala source to $HOME/github/paradise-study/lombok/target/scala-2.12/test-classes ...
getX
getY
getOp_$plus
[info] Done compiling.
// 編譯期結束
// 執行
[info] DataSuite:
[info] - generate setter and getter
[info] - handle operator in the name
[info] Run completed in 437 milliseconds.
[info] Total number of tests run: 2
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 2, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[success] Total time: 4 s, completed 2019-1-1 16:15:43
複製程式碼
小結
本文是Scala超程式設計的一個Case Study,完整的工程見:github.com/sadhen/para…。
最近幾天剛剛學習Scala超程式設計,直覺告訴我,Scala超程式設計並不難,當然,這取決於相關的Domain Knowledge有沒有提前儲備好。