如何在Kotlin DSL构建器中设置必填字段

时间:2018-12-06 12:30:18

标签: kotlin

在Kotlin中,创建自定义DSL时,最好的方法是在编译时强制在构建器的扩展功能内填充必填字段。例如:

person {
    name = "John Doe" // this field needs to be set always, or compile error
    age = 25
}

强制使用此方法的一种方法是在函数参数中设置值,而不是扩展函数的主体。

person(name = "John Doe") {
    age = 25
}

但是如果有更多必填字段,这将使它更加不可读。

还有其他方法吗?

3 个答案:

答案 0 :(得分:3)

New type inference使您可以创建一个空安全的编译时检查生成器:

// Sealed class is used here instead of an interface
// to forbid inheriting it without implementing Named
sealed class PersonBuilder {
    var _name: String? = null
    var age: Int? = null

    // Marker interface which indicates that this PersonBuilder has an initialized name
    interface Named

    // Now we know that each PersonBuilder implements Named
    private class Impl : PersonBuilder(), Named

    companion object {
        // This function invocation looks like constructor invocation
        operator fun invoke(): PersonBuilder = Impl()
    }
}

// Receiver object will be smart casted to <PersonBuilder & Named>
fun PersonBuilder.name(name: String) {
    contract {
        returns() implies (this@name is Named)
    }
    _name = name
}

// Extension property for <PersonBuilder & Named>
val <S> S.name where
        S : PersonBuilder,
        S : Named
    get() = _name!!

// This method can be called only if the builder has been named
fun <S> S.build(): Person where
        S : PersonBuilder,
        S : Named = Person(name, age)

用例:

val builder = PersonBuilder()
// builder.build() // doesn't compile
builder.name("John Doe")
builder.build() // compiles
builder.age = 25
builder.build() // compiles as well

现在让我们添加DSL功能:

// Caller must call build() on the last line of lambda
fun person(init: PersonBuilder.() -> Person) = PersonBuilder().init()

DSL用例:

person {
    // Explicit "this." is required in current Kotlin version for smart casting.
    // It must be a bug which will most likely be fixed later.
    this.name("John Doe") // will not compile without this line
    age = 25
    build()
}

您并不局限于一个必填字段,您可以拥有任意数量的字段。这里还有一个必填字段gender

sealed class PersonBuilder {
    var _name: String? = null
    var _gender: Gender? = null
    var age: Int? = null

    interface Named
    interface WithGender

    private class Impl : PersonBuilder(), Named, WithGender

    companion object {
        operator fun invoke(): PersonBuilder = Impl()
    }
}

fun PersonBuilder.name(name: String) {
    contract {
        returns() implies (this@name is Named)
    }
    _name = name
}

fun PersonBuilder.gender(gender: Gender) {
    contract {
        returns() implies (this@gender is WithGender)
    }
    _gender = gender
}

val <S> S.name where
        S : PersonBuilder,
        S : Named
    get() = _name!!

val <S> S.gender where
        S : PersonBuilder,
        S : WithGender
    get() = _gender!!

fun <S> S.build(): Person where
        S : PersonBuilder,
        S : Named,
        S : WithGender = Person(name, age, gender)

fun person(init: PersonBuilder.() -> Person) = PersonBuilder().init()

此设计还使您能够创建需要在调用某些字段之前对其进行分配的功能。示例:

fun <S> S.genderByName() where
        S : PersonBuilder,
        S : Named {
    contract {
        returns() implies (this@genderByName is WithGender)
    }
    gender(genderOfName(name))
}

用例:

person {
    this.name("John Doe")
    this.genderByName() // will not compile if you call it before "name"
    age = 25
    build()
}

如果您相信如果将sealed类替换为interface,则没人会实现它,那么您可以通过以下方式摆脱一些样板代码:

interface PersonBuilder {
    var _name: String?
    var _gender: Gender?
    var age: Int?

    interface Named : PersonBuilder {
        val name get() = _name!!
    }

    interface WithGender : PersonBuilder {
        val gender get() = _gender!!
    }

    companion object {
        operator fun invoke(): PersonBuilder = object : Named, WithGender {
            override var _name: String? = null
            override var _gender: Gender? = null
            override var age: Int? = null

        }
    }
}

fun PersonBuilder.name(name: String) {
    contract {
        returns() implies (this@name is Named)
    }
    _name = name
}

fun PersonBuilder.gender(gender: Gender) {
    contract {
        returns() implies (this@gender is WithGender)
    }
    _gender = gender
}

fun <S> S.build(): Person where
        S : Named,
        S : WithGender = Person(name, age, gender)

fun person(init: PersonBuilder.() -> Person) = PersonBuilder().init()

fun Named.genderByName() {
    contract {
        returns() implies (this@genderByName is WithGender)
    }
    gender(genderOfName(name))
}

据说在2019年JetBrains开放日,Kotlin团队研究了合同并试图实施合同,以允许在必填字段中创建安全的DSL。 Here是一段谈话录音。此功能甚至还不是实验性功能,因此 也许永远不会将其添加到语言中。

答案 1 :(得分:0)

简单,抛出一个异常,如果它在你的DLS块

在没有定义
fun person(block: (Person) -> Unit): Person {
val p = Person()
block(p)
if (p.name == null) {
  // throw some exception
}
return p
}

或者,如果你想在编译的时候去执行,只要它返回的东西没用到外块如果没有定义,像空。

fun person(block: (Person) -> Unit): Person? {
val p = Person()
block(p)
if (p.name == null) {
  return null
}
return p
}

我猜你要去this example,所以地址可能是更好的例子:

fun Person.address(block: Address.() -> Unit) {
// city is required
var tempAddress = Address().apply(block)
if (tempAddress.city == null) {
   // throw here
}
}

但是,如果我们要确保一切都被定义的,但同时也让你做任何顺序在编译时休息。简单,有两种类型!

data class Person(var name: String = null,
              var age: Int = null,
              var address: Address = null)
data class PersonBuilder(var name: String? = null,
              var age: Int? = null,
              var address: Address? = null)
fun person(block: (PersonBuilder) -> Unit): Person {
    val pb = PersonBuilder()
    block(p)
    val p = Person(pb.name, pb.age, pb.address)
    return p
}

这样,你得到你非严格型打造,但它最好是空少的结束。谢谢,这是一个有趣的问题。

答案 2 :(得分:0)

如果您正在为Android开发,我写了一个轻量级的linter来验证必需的DSL属性。

要解决您的用例,您只需要在@DSLMandatory属性设置器中添加注释name,短绒棉绒在未分配时会抓住任何地方并显示错误:

@set:DSLMandatory
var name: String

err

您可以在这里看看: https://github.com/hananrh/dslint/