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专栏目录

相关文章
相关标签/搜索