若是你讀完了《Scala元編程:伊甸園初窺》,理論上你已經具有實現lombok.Data
的能力了。git
因此,我建議你不要閱讀本文,直接本身嘗試。程序員
@data
class A {
var x: Int = _
var y: String = _
}
複製代碼
咱們但願經過@data
這個註釋,自動生成以下代碼:github
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(二進制哦)種,可是最適合的方式,永遠只有一種。api
我選擇了MyBatis,不採用元編程的手段(其實這段時間我剛剛學會),我是這樣作的:bash
import scala.beans.BeanProperty
class A {
@BeanProperty var x: Int = _
@BeanProperty var y: String = _
}
複製代碼
用Vim列編輯,其實也還好。可是我心裏其實一直在對本身說:DO NOT REPEAT YOURSELF
。ide
// ...
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有沒有提早儲備好。