如何用kotlin写DSL(进阶篇)

在“如何用kotlin写DSL”中,我们将继续上一篇。在这里我们做了一个非常简单的领域专用语言来初始化一个具有Address属性的Person对象。现在,这可能不是开发者应该在现实生活中创建DSL的东西。应该使用DSL来降低复杂性并提高可读性。在我们创建的DSL中没有太多附加东西。不过,这是一个简单的例子,我们将继续从这里解释接下来要做的三件事情:

  • 构建器模式
  • 构造集合
  • 使用DslMarker缩小作用域

之后,我将通过为GsonBuilder创建一个DSL来展示一个真实的例子。

应用构造器模式

所以在前面的部分我们有2个非常基本的数据类,这对于开始的例子是有好处的。但是类有可变的变量,可以很容易地改变它的属性。如果我们可以将它改为不可变的val,会得到一个编译错误,说Val不能被重新分配。为了避免这种情况,可以创建一个类的构建器。可以看看这个模型:


data class Person(val name: String,
                  val dateOfBirth: Date,
                  var address: Address?)

data class Address(val street: String,
                   val number: Int,
                   val city: String)

如果想要创建这些对象,必须使用他们的构造函数的属性值。正如你所看到的,我也添加了一个Date属性。这个将在稍后讨论。

我们要写的构造器非常简单。他们只会构建数据,最后调用构建函数,通过构造函数创建对象。

fun person(block: PersonBuilder.() -> Unit): Person = PersonBuilder().apply(block).build()


class PersonBuilder {
  
    var name: String = ""

    private var dob: Date = Date()
    var dateOfBirth: String = ""
        set(value) {
            dob = SimpleDateFormat("yyyy-MM-dd").parse(value)
        }

    private var address: Address? = null

    fun address(block: AddressBuilder.() -> Unit) {
        address = AddressBuilder().apply(block).build()
    }

    fun build(): Person = Person(name, dob, address)

}

class AddressBuilder {

    var street: String = ""
    var number: Int = 0
    var city: String = ""

    fun build() : Address = Address(street, number, city)

}

如你看到的。我已经添加了一个额外的字符串属性dateOfBirth并使dob私有属性与自定义setter方法,所以我们可以用一个更可读的方式设置日期。因为我们使用的是构造器,所以我们掌握了更多的东西。结果如下:


val person = person {
    name = "John"
    dateOfBirth = "1980-12-01"
    address {
        street = "Main Street"
        number = 12
        city = "London"
    }
}

关于集合

现在我们有了这些构建器,让我们添加一个Collection到我们的模型中。例如,从现在开始,一个人可以有多个地址,这些地址存储在一个List

中。这很容易。我们只需将构建器中的地址属性更改为MutableList
,然后在地址函数中添加一个新地址,并将List传递给构造函数。变化如下:

data class Person(val name: String,
                  val dateOfBirth: Date,
                  val addresses: List
)
class PersonBuilder {
  
  // ... other properties
  
  private val addresses = mutableListOf
() fun address(block: AddressBuilder.() -> Unit) { addresses.add(AddressBuilder().apply(block).build()) } fun build(): Person = Person(name, dob, addresses) }
// And the result
val person = person {
    name = "John"
    dateOfBirth = "1980-12-01"
    address {
        street = "Main Street"
        number = 12
        city = "London"
    }
    address {
        street = "Dev Avenue"
        number = 42
        city = "Paris"
    }
}

所以这很简单。但也许我们不喜欢我们的DSL,我们更希望在地址块内看到它

要做到这一点,可以创建一个地址辅助类。通过这样做,
我们可以把它作为提供在PersonBuilder地址函数lambda的接收器。我已经通过ArrayList扩展了这个辅助类,所以它很容易地添加地址对象。

fun person(block: PersonBuilder.() -> Unit): Person = PersonBuilder().apply(block).build()

class PersonBuilder {

    var name: String = ""
    private var dob: Date = Date()
    var dateOfBirth: String = ""
        set(value) { dob = SimpleDateFormat("yyyy-MM-dd").parse(value) }

    private val addresses = mutableListOf
() fun addresses(block: ADDRESSES.() -> Unit) { addresses.addAll(ADDRESSES().apply(block)) } fun build(): Person = Person(name, dob, addresses) } class ADDRESSES: ArrayList
() { fun address(block: AddressBuilder.() -> Unit) { add(AddressBuilder().apply(block).build()) } } class AddressBuilder { var street: String = "" var number: Int = 0 var city: String = "" fun build() : Address = Address(street, number, city) }

为了强调这只是一个辅助类,可以用大写字母命名。这在DSL中将不可见。这将得到了一个很好的结构。

val person = person {
    name = "John"
    dateOfBirth = "1980-12-01"
    addresses {
        address {
            street = "Main Street"
            number = 12
            city = "London"
        }
        address {
            street = "Dev Avenue"
            number = 42
            city = "Paris"
        }
    }
}

缩小作用域

这个结果看起来不错。它的可读性,可维护性和使用安全性得益于带有接收者的lambda表达式。但是有一个问题

因使用DSL时,可能会遇到上下文中可以调用太多函数的问题。 我们可以调用 lambda 表达式内部每个可用的隐式接收者的方法,因此得到一个不一致的结果:

val person = person {
    name = "John"
    dateOfBirth = "1980-12-01"
    addresses {
        address {
            addresses { 
                name = "Mary"
            }
            street = "Dev Avenue"
            number = 42
            city = "Paris"
        }
    }
}

如果我们运行这个,person.name是“Mary”,而不是“John”。幸运的是,从Kotlin 1.1开始,我们可以通过@DslMarker注释来避免这种情况。这个注解应该被应用到一个自定义的注释类,然后你注释你的DSL类。

在添加了这个注解之后,Kotlin 编译器就知道哪些隐式接收者是同一个 DSL 的一部分,并且只允许调用最近层的接收者的成员

@DslMarker
annotation class PersonDsl

@PersonDsl
class PersonBuilder {
  //...
}

@PersonDsl
class ADDRESSES: ArrayList
() { //... } @PersonDsl class AddressBuilder { //... }

现在编译器给了最后一个例子的一个错误。地址和名称不能在这个上下文中由隐式接收器调用。如果想要的话,仍然可以在内部lambda表达式中使用一个显式的接收者:[email protected] = “Mary”

实例:简化GsonBuilder

在这最后一个例子中,我将展示一个快速的例子,我已经创建了自己的内部DSL,用于一个真实的例子:GsonBuilder

如果你想创建一个序列化和反序列化的gson实例,库已经为你提供了一个GsonBuilder来轻松的配置实例。但是,如果您想跳过(de)序列化字段或类。它变得相当混乱。以这个例子

val gson = GsonBuilder()
          .addDeserializationExclusionStrategy(object: ExclusionStrategy {
              override fun shouldSkipClass(clazz: Class<*>?): Boolean {
                  return clazz?.equals(Address::class.java) ?: false
              }

              override fun shouldSkipField(f: FieldAttributes?): Boolean {
                  return f?.let { it.name == "internalId" } ?: false
              }

          })
          .addSerializationExclusionStrategy(object: ExclusionStrategy {
              override fun shouldSkipClass(clazz: Class<*>?): Boolean {
                  return false
              }

              override fun shouldSkipField(f: FieldAttributes?): Boolean {
                  return f?.let { it.declaringClass == Person::class.java && it.name == "address" } ?: false
              }

          })
          .serializeNulls()
          .create()

这个Gson对象排除了反序列化的Address类和任何名为internalId的字段。它也排除了序列化Person.address。问题在于它的可读性。

即使函数名addDeserializationExlusionStrategy看起来很简单。但由于Kotlin的特性,比如elvis操作符 ?:这已经比Java中的要短,但我们可以做得更好。
通过使用我们在本文最后两部分中讨论的Kotlin语言功能,我们能够创建一个DSL,并具有相同的代码,如下所示:

val  gson  = gson {
    dontDeserialize {
        whenField {name ==  “ internalId ” }
        whenClass {等号(地址::类。 JAVA)}
    }
    dontSerialize {
        whenField {declaringClass == Person :: class。java && name ==“ address ”}
    }
    serializeNulls()
}

结果是更可读,更易理解和更简洁。看看如何实现的:

fun gson(block: GsonBuilder.() -> Unit): Gson = GsonBuilder().apply(block).create()

fun GsonBuilder.dontDeserialize(block: ExclusionStrategyBuilder.() -> Unit) {
    val strategy = ExclusionStrategyBuilder().apply(block).build()
    this.addDeserializationExclusionStrategy(strategy)
}

fun GsonBuilder.dontSerialize(block: ExclusionStrategyBuilder.() -> Unit) {
    val strategy = ExclusionStrategyBuilder().apply(block).build()
    this.addSerializationExclusionStrategy(strategy)
}

class ExclusionStrategyBuilder {

    private var field: (FieldAttributes) -> Boolean = { false }
    private var clazz: (Class<*>) -> Boolean? = { false }

    fun whenField(block: FieldAttributes.() -> Boolean) {
        field = block
    }

    fun whenClass(block: Class<*>.() -> Boolean) {
        clazz = block
    }

    fun build(): ExclusionStrategy {
        return object : ExclusionStrategy {
            override fun shouldSkipClass(clazz: Class<*>?): Boolean {
                return clazz?.let { clazz(it) } ?: false
            }

            override fun shouldSkipField(f: FieldAttributes?): Boolean {
                return f?.let { field(it) } ?: false
            }

        }
    }
}

不幸的是,现在无法在这里添加@DslMarker注解。GsonBuilder是不可变的,也不能扩展它。尽管如此,这使得构建Gson实例变得更加容易。

你可能感兴趣的:(如何用kotlin写DSL(进阶篇))