在 2019 年 JetBrains 开放日上,据说 Kotlin 团队研究了合约并试图实现context允许仅在某些上下文中调用函数的合约,例如函数build
仅当以下情况时才允许被调用setName
方法在它之前被调用过一次。Here https://youtu.be/g0ivd1TMu9s?t=1579是一个谈话录音。
我尝试使用当前可用的 Kotlin 功能来模拟此类合约,以创建空安全构建器data class Person(val name: String, val age: Int)
.
注意:当然,在这种情况下,使用命名参数而不是构建器模式要容易得多,但是命名参数不允许将未完全构建的对象解析为其他函数,并且当您想要创建时很难使用它们由其他复杂对象等组成的复杂对象。
这是我的空安全构建器实现:
基于通用标志的构建器
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)
Pros:
- 一个人不能被建立,除非两者都具备
name
and age
被指定。
- 属性无法重新分配。
- 部分构建的对象可以安全地保存到变量并传递给函数。
- 函数可以指定构建器所需的状态以及将返回的状态。
- 属性可以在赋值后使用。
- 界面流畅。
Cons:
- 该构建器不能与 DSL 一起使用。
- 如果不添加类型参数并破坏所有现有代码,则无法添加新属性。
- 每次都必须指定所有泛型(即使函数不关心
age
,它必须声明它接受具有任何AGE
类型参数并返回具有相同类型参数的构建器。)
-
_name
and _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)
基于合同的建造者
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)
Pros:
- 与 DSL 兼容。
- 在指定姓名和年龄之前,无法构建人。
- 仅必须指定已更改和所需的接口。 (没有
Aged
提及name
功能。)
- 可以轻松添加新属性。
- 部分构建的对象可以安全地保存到变量并传递给函数。
- 属性可以在赋值后使用。
Cons:
- 带有接收器的 Lambda 不能在 DSL 中使用,因为 Kotlin 不会推断 Lambda 的类型
this
参考。
- 属性可以重新分配。
- 样板代码位于
where
clause.
- 无法显式指定变量类型(
PersonBuilder & Named
不是有效的 Kotlin 语法)。
-
_name
and _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()
有没有更好的空安全构建器实现,有什么方法可以摆脱我的实现缺点?