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

2024-05-22

在 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:

  1. 一个人不能被建立,除非两者都具备name and age被指定。
  2. 属性无法重新分配。
  3. 部分构建的对象可以安全地保存到变量并传递给函数。
  4. 函数可以指定构建器所需的状态以及将返回的状态。
  5. 属性可以在赋值后使用。
  6. 界面流畅。

Cons:

  1. 该构建器不能与 DSL 一起使用。
  2. 如果不添加类型参数并破坏所有现有代码,则无法添加新属性。
  3. 每次都必须指定所有泛型(即使函数不关心age,它必须声明它接受具有任何AGE类型参数并返回具有相同类型参数的构建器。)
  4. _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:

  1. 与 DSL 兼容。
  2. 在指定姓名和年龄之前,无法构建人。
  3. 仅必须指定已更改和所需的接口。 (没有Aged提及name功能。)
  4. 可以轻松添加新属性。
  5. 部分构建的对象可以安全地保存到变量并传递给函数。
  6. 属性可以在赋值后使用。

Cons:

  1. 带有接收器的 Lambda 不能在 DSL 中使用,因为 Kotlin 不会推断 Lambda 的类型this参考。
  2. 属性可以重新分配。
  3. 样板代码位于where clause.
  4. 无法显式指定变量类型(PersonBuilder & Named不是有效的 Kotlin 语法)。
  5. _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()

有没有更好的空安全构建器实现,有什么方法可以摆脱我的实现缺点?


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

我的建议:

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!

Pros:

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

Cons:

  1. 仅用于一个属性/字段的样板代码/类
  2. 接收者类必须知道一个特定的(不同的)类,而不是一个类。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何强制客户端代码使用合约初始化 Kotlin 中所有必需的构建器字段? 的相关文章

随机推荐

  • C++ 中的 Anderson Darling 测试

    我正在尝试计算发现的安德森 达林测试here https en wikipedia org wiki Anderson E2 80 93Darling test 我按照维基百科上的步骤进行操作 并确保当我计算我正在测试的数据的平均值和标准差
  • Android Surface 与 Canvas 的关系

    Surface 和 Canvas 之间到底是什么关系 请解释 表面是一个缓冲区 画布保存绘图 视图未附加到画布 也不是表面 窗户被绑在 Surface 和 ViewRoot 询问 随后使用的画布表面 通过要绘制的视图 详细答案你可以阅读这篇
  • IBOutlet、实例变量和属性:最佳实践

    今天 我对有关声明 IBOutlet 和实例变量 管理它们 使用正确的访问器以及正确释放它们的最佳实践进行了各种研究 我已经差不多了 但我有一些小问题 我希望有人能够就最佳实践提出建议 我会将它们格式化为代码并注释问题 以便更容易理解 我排
  • Git checkout 不会丢弃我的更改

    我在 Windows XP 上使用 git 1 7 1 和 cygwin 这个问题可以通过例子得到最好的说明 git status On branch master Changed but not updated use git add
  • Three20中的TTSpeechBubbleShape仅绘制“语音”三角形顶部和底部

    因此 我将 Three20 库用于 iPhone 应用程序 并希望将 TTSpeechBubbleShape 样式用于视图 但三角形似乎不想画在左边或右边 我在源代码中看到它有很多几何图形 并且想知道是否有人解决了这个问题或知道如何解决它
  • python 中的泛型方法

    是否有可能在Python中实现通用方法处理程序 允许调用不存在的函数 像这样的东西 class FooBar def generic method handler methodName print methodName fb FooBar
  • 如何在java中以编程方式访问网页

    有一个网页 我想从中检索某个字符串 为此 我需要登录 单击一些按钮 填充文本框 单击另一个按钮 然后就会出现字符串 我怎样才能编写一个java程序来自动执行此操作 是否有任何有用的库用于此目的 Thanks Try HtmlUnit htt
  • django过滤器查询集在模板上显示变量[重复]

    这个问题在这里已经有答案了 下面是我的统计页面的views py 此页面有大量基于我的模型对象的计算 效果很好 然而当我申请时Django 过滤器 https django filter readthedocs io en stable 数
  • Robolectric 1.2:“警告:无法找到 Android SDK 的路径”

    I used Robolectric 1 1 jar 与依赖项 在我的项目中并成功使其工作 但是当我将罐子更改为 1 2 SNAPSHOT jar 与依赖项 我收到以下警告 警告 无法找到 Android SDK 的路径 两个jar包都下载
  • 是否有必要将 using 与 IDisposable 对象嵌套?

    我必须把我所有的包裹起来吗IDisposable对象在using 声明 即使我只是将一个声明传递给另一个声明 例如 在以下方法中 public static string ReadResponse HttpWebResponse respo
  • iOS8beta5中无法使用UIWebView打开PDF文件

    I have 工作项目我在其中显示UIWebView 中的 pdf 文件在测试我的应用程序时iOS8beta5 与 XCode5 它不起作用 In log它显示failed to find PDF header PDF not found
  • ASP.NET HTTP 请求是否会转换为 1 个线程?

    可以安全地假设当用户通过 HTTP 请求 aspx 页面时 ASP NET 至少为其创建 1 个线程吗 如果是这样 持续多久 如果 1000 人向同一个 aspx 页面发出 HTTP 请求 是否会涉及一些线程回收 因此不会产生不同的 100
  • Pycharm 中的 Traitlets.traitlets.TraitError

    我是Python的初学者 我面临以下问题 每当我启动 pycharm 社区版 版本 5 0 3 时 Python 控制台无法启动并显示以下错误 usr bin python2 7 usr lib pycharm community help
  • 为什么我会收到此 Javascript 错误“连接未定义”?

    我不确定为什么会收到此错误 connection is not defined document getElementById flashTest sendValFromHtml connection value 这是我的代码 functi
  • 使用 enctype="multipart/form-data" 时出现 CSRF 错误

    每当我将 enctype multipart form data 添加到我的 html 表单中时 我得到ForbiddenError invalid csrf token 如果我删除 enctype 它就可以工作 我发送的 csrf 代码如
  • json_encode 返回 200 且未定义

    我想要一个代码来添加或删除数据库书签 代码已准备就绪 它可以正确地从数据库书签中添加和删除书签 但是当我调用该函数时 它会不断返回json error反而json success即使代码有效 我想知道代码 我从其他地方获得并改编的 有什么问
  • 如何在状态更改时更改 Android 切换按钮的文本颜色?

    我的切换按钮对于每个状态都有不同的颜色背景 红色和白色 现在我需要在激活时更改切换按钮文本 红色 白色 的颜色 使用 xml 我无法让它工作 也许有人知道我做错了什么 我的布局 xml 中的按钮
  • jquery选中复选框IE问题

    我在验证是否使用 Internet Explorer 上的 jquery 选中复选框时遇到问题 这是我正在使用的代码 if chkjq 1 attr checked true 它在 Firefox 或 Chrome 上运行良好 但在 Int
  • 对编码的 XML 值应用 XSLT 模板

    我有一个 XML 文档 需要将其转换为 HTML XML内容如下
  • 如何强制客户端代码使用合约初始化 Kotlin 中所有必需的构建器字段?

    在 2019 年 JetBrains 开放日上 据说 Kotlin 团队研究了合约并试图实现context允许仅在某些上下文中调用函数的合约 例如函数build仅当以下情况时才允许被调用setName方法在它之前被调用过一次 Here ht