Scala元編程:實現lombok.Data

若是你讀完了《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有沒有提早儲備好。


返回閒話Scala專欄目錄

相關文章
相關標籤/搜索