1. 程式人生 > >Scala超程式設計:實現lombok.Data

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有沒有提前儲備好。