若是你讀完了《Scala元編程:伊甸園初窺》,理論上你已經具有實現lombok.Data的能力了。git
因此,我建議你不要閱讀本文,直接本身嘗試。程序員
lombok.Data
的 Scala 版@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(二進制哦)種,可是最適合的方式,永遠只有一種。我選擇了MyBatis,不採用元編程的手段(其實這段時間我剛剛學會),我是這樣作的:segmentfault
import scala.beans.BeanProperty class A { @BeanProperty var x: Int = _ @BeanProperty var y: String = _ }
用Vim列編輯,其實也還好。可是我心裏其實一直在對本身說:DO NOT REPEAT YOURSELF。api
// ... 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") } } // ...
上一篇元編程相關的文章實際上主要是爲了強調構建,因此我貼了兩次構建定義的代碼。ide
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,完整的工程見:https://github.com/sadhen/par...。
最近幾天剛剛學習Scala元編程,直覺告訴我,Scala元編程並不難,固然,這取決於相關的Domain Knowledge有沒有提早儲備好。