在“如何用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实例变得更加容易。