如何通过合同强制客户端代码初始化Kotlin中所有必需的构建器字段?

Ban*_*non 5 generics type-inference contract kotlin

据说在2019年的JetBrains开放日,Kotlin团队研究了合同并试图实现上下文合同,该合同仅允许在某些情况下调用函数,例如,build仅在setName方法调用前被调用了一次的情况下才允许调用函数。是一段谈话录音。

我尝试使用当前可用的Kotlin功能模拟此类合同,以为创建null安全构建器data class Person(val name: String, val age: Int)

注意:当然,在这种情况下,使用命名参数而不是生成器模式要容易得多,但是命名参数不允许将未完全构建的对象解析为其他函数,并且在创建时很难使用它们由其他复杂对象等组成的复杂对象。

所以这是我的null安全构建器实现:

基于通用标志的构建器

sealed class Flag {
    object ON : Flag()
    object OFF : Flag()
}

class PersonBuilder<NAME : Flag, AGE : Flag> private constructor() {
    var _name: String? = null
    var _age: Int? = null

    companion object {
        operator fun invoke() = PersonBuilder<OFF, OFF>()
    }
}

val PersonBuilder<ON, *>.name get() = _name!!
val PersonBuilder<*, ON>.age get() = _age!!

fun <AGE : Flag> PersonBuilder<OFF, AGE>.name(name: String): PersonBuilder<ON, AGE> {
    _name = name
    @Suppress("UNCHECKED_CAST")
    return this as PersonBuilder<ON, AGE>
}

fun <NAME : Flag> PersonBuilder<NAME, OFF>.age(age: Int): PersonBuilder<NAME, ON> {
    _age = age
    @Suppress("UNCHECKED_CAST")
    return this as PersonBuilder<NAME, ON>
}

fun PersonBuilder<ON, ON>.build() = Person(name, age)
Run Code Online (Sandbox Code Playgroud)

优点:

  1. 必须同时指定name和才能建立人age
  2. 无法重新分配属性。
  3. 可以将部分构建的对象安全地保存到变量中并传递给函数。
  4. 函数可以指定生成器的必需状态和将返回的状态。
  5. 分配后可以使用属性。
  6. 流利的界面。

缺点:

  1. 该生成器不能与DSL一起使用。
  2. 如果不添加类型参数并破坏所有现有代码,则无法添加新属性。
  3. 必须每次都指定所有泛型(即使某个函数无关紧要age,它也必须声明它接受具有任何AGE类型参数的构建器,并返回具有相同类型参数的构建器。)
  4. _name并且_age属性不能是私有的,因为应该可以从扩展功能访问它们。

这是此构建器用法示例:

PersonBuilder().name("Bob").age(21).build()
PersonBuilder().age(21).name("Bob").build()
PersonBuilder().name("Bob").name("Ann") // doesn't compile
PersonBuilder().age(21).age(21) // doesn't compile
PersonBuilder().name("Bob").build() // doesn't compile
PersonBuilder().age(21).build() // doesn't compile

val newbornBuilder = PersonBuilder().newborn() // builder with age but without name
newbornBuilder.build() // doesn't compile
newbornBuilder.age(21) // doesn't compile
val age = newbornBuilder.age
val name = newbornBuilder.name // doesn't compile
val bob = newbornBuilder.name("Bob").build()
val person2019 = newbornBuilder.nameByAge().build()
PersonBuilder().nameByAge().age(21).build() // doesn't compile

fun PersonBuilder<OFF, ON>.nameByAge() = name("Person #${Year.now().value - age}")
fun <NAME : Flag> PersonBuilder<NAME, OFF>.newborn() = age(0)
Run Code Online (Sandbox Code Playgroud)

基于合同的构建器

sealed class PersonBuilder {
    var _name: String? = null
    var _age: Int? = null

    interface Named
    interface Aged

    private class Impl : PersonBuilder(), Named, Aged

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

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

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

fun PersonBuilder.age(age: Int) {
    contract {
        returns() implies (this@age is Aged)
    }
    _age = age
}

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

fun <R> newPerson(init: PersonBuilder.() -> R): Person
        where R : Named,
              R : Aged,
              R : PersonBuilder =
    PersonBuilder().run(init).build()

fun <R> itPerson(init: (PersonBuilder) -> R): Person
        where R : Named,
              R : Aged,
              R : PersonBuilder =
    newPerson(init)
Run Code Online (Sandbox Code Playgroud)

优点:

  1. 与DSL兼容。
  2. 必须同时指定姓名和年龄,才能建造一个人。
  3. 仅必须指定更改的和必需的接口。(函数中未Aged提及name。)
  4. 可以轻松添加新属性。
  5. 可以将部分构建的对象安全地保存到变量中并传递给函数。
  6. 分配后可以使用属性。

缺点:

  1. 带接收器的Lambda不能在DSL中使用,因为Kotlin不会推断this引用的类型。
  2. 可以重新分配属性。
  3. where子句中的样板代码。
  4. 无法明确指定变量类型(PersonBuilder & Named这不是有效的Kotlin语法)。
  5. _name并且_age属性不能是私有的,因为应该可以从扩展功能访问它们。

这是此构建器用法示例:

newPerson {
    age(21)
    name("Bob")
    this // doesn't compile (this type isn't inferred)
}
itPerson {
    it.age(21)
    it.name("Ann")
    it
}
itPerson {
    it.age(21)
    it // doesn't compile
}
val builder = PersonBuilder()
builder.name("Bob")
builder.build() // doesn't compile
builder.age(21)
builder.build()
Run Code Online (Sandbox Code Playgroud)

是否有更好的null安全生成器实现,并且有什么方法摆脱我的实现弊端?

Neo*_*Neo 0

我认为合同不适合您的问题,而建筑商“组合”可能适合。

我的建议:

class PersonBuilder(private val name: String, private val age: Int) {
    fun build() = Person(name, age)
}

class PersonNameBuilder(private val name: String) {

    fun withAge(age: Int) = PersonBuilder(name, age)
}

class PersonAgeBuilder(private val age: Int) {

    fun withName(name: String) = PersonBuilder(name, age)
}

data class Person(val name: String, val age: Int)
Run Code Online (Sandbox Code Playgroud)

用例:

PersonNameBuilder("Bob").withAge(13).build() 
PersonAgeBuilder(25).withName("Claire").build()

PersonNameBuilder("Bob") // can't build(). Forced to add age!
PersonAgeBuilder(25) // can't build(). Forced to add name!
Run Code Online (Sandbox Code Playgroud)

优点:

  1. 在指定姓名和年龄之前无法构建人
  2. 属性无法重新分配。
  3. 部分构建的对象可以安全地保存到变量并传递给函数
  4. 流畅的界面
  5. 非常容易扩展、更改、重构,可以使用 Labda 和惰性执行
  6. DSL可以轻松完成
  7. 如果使用 Labda 进行丰富,可以在后台调用或执行某些内容 - 非常容易测试,因为它位于自己的单个类中
  8. 如果需要,可以添加泛型

缺点:

  1. 仅用于一个属性/字段的样板代码/类
  2. 接收者类必须知道一个特定的(不同的)类,而不是一个类。

  • 我不太明白你的意思。无论如何,通过使用 `MutableMap&lt;Any?, Boolean&gt;` _(或 `MutableSet&lt;Any?&gt;`)_ 你只能实现运行时验证,并且我希望在错误使用的情况下得到编译时错误一名建筑商。注意:最好使用属性委托而不是地图。 (2认同)