据说在2019年的JetBrains开放日,Kotlin团队研究了契约(Contract)并试图实现上下文契约(Contract),该契约(Contract)仅允许在某些上下文中调用函数,例如,仅当build方法恰好被调用一次时才允许调用setName函数在它之前。 Here是谈话录音。

我尝试使用当前可用的Kotlin功能来模拟此类契约(Contract),以便为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)

优点:
  • 必须同时指定nameage才能构建人。
  • 无法重新分配属性。
  • 可以将部分构建的对象安全地保存到变量中并传递给函数。
  • 函数可以指定生成器的必需状态以及将返回的状态。
  • 属性可以在分配后使用。
  • Fluent界面。

  • 缺点:
  • 此构建器不能与DSL一起使用。
  • 如果不添加类型参数并破坏所有现有代码,则无法添加新属性。
  • 必须每次都指定所有泛型(即使函数不关心age,它也必须声明它接受具有任何AGE类型参数的构建器,并返回具有相同类型参数的构建器。)
  • _name_age属性不能为私有(private)属性,因为应该可以从扩展功能访问它们。

  • 这是此构建器的使用示例:
    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)
    

    基于契约(Contract)的构建器
    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)
    

    优点:
  • 与DSL兼容。
  • 必须同时指定姓名和年龄才能 build 人。
  • 仅必须指定更改的接口(interface)和必需的接口(interface)。 (在Aged函数中没有提及name。)
  • 可以轻松添加新属性。
  • 可以将部分构建的对象安全地保存到变量中并传递给函数。
  • 分配后可以使用属性。

  • 缺点:

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

  • 这是此构建器的使用示例:
    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()
    

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

    最佳答案

    我认为契约(Contract)不适合您的问题,而 build 商的“组合”可能适合。

    我的建议:

    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)
    

    用例:
    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!
    

    优点:
  • 必须同时指定姓名和年龄才能建立人
  • 无法重新分配属性。
  • 可以将部分构建的对象安全地保存到变量中并传递给函数
  • Fluent界面
  • 非常容易扩展,更改,重构,例如使用labdas和惰性执行
  • DSL可以轻松完成
  • 如果其中包含Labdas以便在后台调用或执行某些内容,则非常容易测试,因为它位于自己的单个类中
  • 如果需要,可以添加泛型

    缺点:
  • 样板代码/仅用于一个属性的类/字段
  • 接收器类必须知道一个特定的(不同的)类,而不是一个类。
  • 09-11 19:14
    查看更多