如何在 Kotlin 中实现 Builder 模式?
Posted
技术标签:
【中文标题】如何在 Kotlin 中实现 Builder 模式?【英文标题】:How to implement Builder pattern in Kotlin? 【发布时间】:2016-07-08 12:54:02 【问题描述】:您好,我是 Kotlin 世界的新手。我喜欢我目前所看到的,并开始考虑将我们在应用程序中使用的一些库从 Java 转换为 Kotlin。
这些库充满了带有 setter、getter 和 Builder 类的 Pojo。现在我用谷歌搜索找到在 Kotlin 中实现构建器的最佳方法,但没有成功。
第二次更新:问题是如何在 Kotlin 中为带有一些参数的简单 pojo 编写 Builder 设计模式?下面的代码是我尝试编写java代码,然后使用eclipse-kotlin-plugin转换成Kotlin。
class Car private constructor(builder:Car.Builder)
var model:String? = null
var year:Int = 0
init
this.model = builder.model
this.year = builder.year
companion object Builder
var model:String? = null
private set
var year:Int = 0
private set
fun model(model:String):Builder
this.model = model
return this
fun year(year:Int):Builder
this.year = year
return this
fun build():Car
val car = Car(this)
return car
【问题讨论】:
你需要model
和year
是可变的吗?在创建Car
之后您会更改它们吗?
我猜它们应该是不可变的。另外,您要确保它们都设置了而不是空的
您也可以使用这个github.com/jffiorillo/jvmbuilder Annotation Processor 自动为您生成构建器类。
@JoseF 将它添加到标准 kotlin 的好主意。它对于用 kotlin 编写的库很有用。
大多数答案都忽略了构建器的一个基本但重要的用途,即增量构建一个不可变对象。这有无数种用途,例如,在解析输入时。在这种情况下,为每个事件创建一个新的数据类将是完全浪费的。
【参考方案1】:
首先,在大多数情况下,您不需要在 Kotlin 中使用构建器,因为我们有默认参数和命名参数。这使您可以编写
class Car(val model: String? = null, val year: Int = 0)
并像这样使用它:
val car = Car(model = "X")
如果您绝对想使用构建器,可以这样做:
将 Builder 设为 companion object
没有意义,因为 object
s 是单例。而是将其声明为嵌套类(在 Kotlin 中默认为静态)。
将属性移动到构造函数,以便对象也可以以常规方式实例化(如果不应该将构造函数设为私有)并使用辅助构造函数,该构造函数接受构建器并委托给主构造函数。代码如下所示:
class Car( //add private constructor if necessary
val model: String?,
val year: Int
)
private constructor(builder: Builder) : this(builder.model, builder.year)
class Builder
var model: String? = null
private set
var year: Int = 0
private set
fun model(model: String) = apply this.model = model
fun year(year: Int) = apply this.year = year
fun build() = Car(this)
用法:val car = Car.Builder().model("X").build()
此代码可以通过使用builder DSL 来缩短:
class Car (
val model: String?,
val year: Int
)
private constructor(builder: Builder) : this(builder.model, builder.year)
companion object
inline fun build(block: Builder.() -> Unit) = Builder().apply(block).build()
class Builder
var model: String? = null
var year: Int = 0
fun build() = Car(this)
用法:val car = Car.build model = "X"
如果某些值是必需的并且没有默认值,则需要将它们放入构建器的构造函数中,以及我们刚刚定义的build
方法中:
class Car (
val model: String?,
val year: Int,
val required: String
)
private constructor(builder: Builder) : this(builder.model, builder.year, builder.required)
companion object
inline fun build(required: String, block: Builder.() -> Unit) = Builder(required).apply(block).build()
class Builder(
val required: String
)
var model: String? = null
var year: Int = 0
fun build() = Car(this)
用法:val car = Car.build(required = "requiredValue") model = "X"
【讨论】:
没什么,但是题主特意问了如何实现builder模式。 我应该纠正自己,构建器模式有一些优点,例如您可以将部分构造的构建器传递给另一种方法。但你说得对,我会补充一句。 @KirillRakhman 从 java 调用构建器怎么样?有没有一种简单的方法可以让构建器对 java 可用? 所有三个版本都可以从 Java 中调用,如下所示:Car.Builder builder = new Car.Builder();
。然而,只有第一个版本有一个流畅的接口,所以对第二个和第三个版本的调用不能被链接。
我认为顶部的 kotlin 示例仅说明了一种可能的用例。我使用构建器的主要原因是将可变对象转换为不可变对象。也就是说,我需要在“构建”时随着时间的推移对其进行变异,然后提出一个不可变的对象。至少在我的代码中,只有一两个代码示例具有如此多的参数变化,以至于我会使用构建器而不是几个不同的构造器。但是为了制作一个不可变的对象,我有几个案例,构建器绝对是我能想到的最干净的方式。【参考方案2】:
一种方法是执行以下操作:
class Car(
val model: String?,
val color: String?,
val type: String?)
data class Builder(
var model: String? = null,
var color: String? = null,
var type: String? = null)
fun model(model: String) = apply this.model = model
fun color(color: String) = apply this.color = color
fun type(type: String) = apply this.type = type
fun build() = Car(model, color, type)
使用示例:
val car = Car.Builder()
.model("Ford Focus")
.color("Black")
.type("Type")
.build()
【讨论】:
非常感谢!你让我今天一整天都感觉很好!您的答案应标记为 SOLUTION。 但是为什么呢?这在 Kotlin 中是不必要的,臃肿,不安全且容易出错。 :( 你甚至可以通过提供一个 init 块来做一些验证。请不要将过时的 Java 模式强加到 Kotlin 中。 因为您可能需要在 Java 代码中实例化 Car 类。【参考方案3】:我个人从未见过 Kotlin 的构建器,但也许只有我一个人。
所有需要的验证都发生在init
块中:
class Car(val model: String,
val year: Int = 2000)
init
if(year < 1900) throw Exception("...")
在这里,我冒昧地猜测您并不真的希望 model
和 year
可以更改。而且这些默认值似乎没有意义,(尤其是null
用于name
)但我留下了一个用于演示目的。
意见: Java 中使用的构建器模式是一种在没有命名参数的情况下生存的方法。在具有命名参数的语言(如 Kotlin 或 Python)中,让构造函数具有长列表(可能是可选的)参数是一个好习惯。
【讨论】:
非常感谢您的回答。我喜欢你的方法,但缺点是对于一个有很多参数的类,使用构造函数和测试类变得不太友好。 +Keyhan 您可以通过另外两种方式进行验证,假设验证不会在字段之间发生:1) 在 setter 进行验证的地方使用属性委托 - 这与拥有进行验证的普通 setter 2) 避免对原始的痴迷并创建新的类型以传入验证自己。 @Keyhan 这是 Python 中的经典方法,即使对于具有数十个参数的函数也能很好地工作。这里的技巧是使用命名参数(Java 中不可用!) 是的,这也是一个值得使用的解决方案,似乎不像java那样builder类有一些明显的优势,在Kotlin中就不是那么明显了,和C#开发者谈过,C#也有类似kotlin的特性(默认值,您可以在调用构造函数时命名参数)它们也没有使用构建器模式。 @vxh.viet 很多这样的情况都可以用@JvmOverloads
kotlinlang.org/docs/reference/…解决【参考方案4】:
因为我使用 Jackson 库从 JSON 中解析对象,所以我需要一个空的构造函数并且我不能有可选字段。此外,所有字段都必须是可变的。然后我可以使用这种与 Builder 模式做同样事情的好语法:
val car = Car().apply model = "Ford"; year = 2000
【讨论】:
在杰克逊你实际上不需要有一个空的构造函数,并且字段不需要是可变的。您只需使用@JsonProperty
注释您的构造函数参数
如果您使用-parameters
开关编译,您甚至不必再使用@JsonProperty
进行注释。
杰克逊实际上可以配置为使用构建器。
如果将jackson-module-kotlin模块添加到您的项目中,您只需使用数据类就可以了。
这如何与构建器模式做同样的事情?您正在实例化最终产品,然后换出/添加信息。 Builder 模式的重点是在所有必要信息都存在之前无法获得最终产品。删除 .apply() 会留下一辆未定义的汽车。从 Builder 中删除所有构造函数参数会为您留下 Car Builder,如果您尝试将其构建到汽车中,您可能会因为尚未指定型号和年份而遇到异常。它们不是一回事。【参考方案5】:
我已经看到了许多声明作为建设者的额外乐趣的例子。我个人喜欢这种方法。节省编写构建器的工作量。
package android.zeroarst.lab.koltinlab
import kotlin.properties.Delegates
class Lab
companion object
@JvmStatic fun main(args: Array<String>)
val roy = Person
name = "Roy"
age = 33
height = 173
single = true
car
brand = "Tesla"
model = "Model X"
year = 2017
car
brand = "Tesla"
model = "Model S"
year = 2018
println(roy)
class Person()
constructor(init: Person.() -> Unit) : this()
this.init()
var name: String by Delegates.notNull()
var age: Int by Delegates.notNull()
var height: Int by Delegates.notNull()
var single: Boolean by Delegates.notNull()
val cars: MutableList<Car> by lazy arrayListOf<Car>()
override fun toString(): String
return "name=$name, age=$age, " +
"height=$height, " +
"single=$when (single)
true -> "looking for a girl friend T___T"
false -> "Happy!!"
\nCars: $cars"
class Car()
var brand: String by Delegates.notNull()
var model: String by Delegates.notNull()
var year: Int by Delegates.notNull()
override fun toString(): String
return "(brand=$brand, model=$model, year=$year)"
fun Person.car(init: Car.() -> Unit): Unit
cars.add(Car().apply(init))
我还没有找到一种方法可以强制在 DSL 中初始化某些字段,例如显示错误而不是抛出异常。如果有人知道,请告诉我。
【讨论】:
【参考方案6】:对于一个简单的类,您不需要单独的构建器。您可以使用 Kirill Rakhman 描述的可选构造函数参数。
如果你有更复杂的类,那么 Kotlin 提供了一种创建 Groovy 风格的 Builders/DSL 的方法:
Type-Safe Builders
这是一个例子:
Github Example - Builder / Assembler
【讨论】:
谢谢,但我也想从 java 中使用它。据我所知,可选参数不适用于 java。【参考方案7】:现在人们应该查看 Kotlin 的 Type-Safe Builders。
使用上述对象创建方式将如下所示:
html
head
title +"XML encoding with Kotlin"
// ...
一个很好的“实际”使用示例是vaadin-on-kotlin 框架,它利用类型安全构建器来assemble views and components。
【讨论】:
【参考方案8】:我想说,Kotlin 中的模式和实现几乎相同。由于默认值,您有时可以跳过它,但对于更复杂的对象创建,构建器仍然是一个不能省略的有用工具。
【讨论】:
对于具有默认值的构造函数,您甚至可以使用initializer blocks 验证输入。但是,如果您需要有状态的东西(这样您就不必预先指定所有内容),那么构建器模式仍然是可行的方法。 你能给我一个简单的例子吗?说一个简单的用户类,其中包含名称和电子邮件字段以及电子邮件验证。【参考方案9】:我正在处理一个 Kotlin 项目,该项目公开了 Java 客户端使用的 API(无法利用 Kotlin 语言结构)。我们必须添加构建器以使它们在 Java 中可用,所以我创建了一个 @Builder 注释:https://github.com/ThinkingLogic/kotlin-builder-annotation - 它基本上是 Kotlin 的 Lombok @Builder 注释的替代品。
【讨论】:
【参考方案10】:我迟到了。如果我必须在项目中使用 Builder 模式,我也遇到了同样的困境。后来,经过研究,我意识到这是绝对没有必要的,因为 Kotlin 已经提供了命名参数和默认参数。
如果您真的需要实施,Kirill Rakhman 的回答是关于如何以最有效的方式实施的可靠答案。您可能会发现它有用的另一件事是https://www.baeldung.com/kotlin-builder-pattern,您可以在实现上与 Java 和 Kotlin 进行比较和对比
【讨论】:
【参考方案11】:class Foo private constructor(@DrawableRes requiredImageRes: Int, optionalTitle: String?)
@DrawableRes
@get:DrawableRes
val requiredImageRes: Int
val optionalTitle: String?
init
this.requiredImageRes = requiredImageRes
this.requiredImageRes = optionalTitle
class Builder
@DrawableRes
private var requiredImageRes: Int = -1
private var optionalTitle: String? = null
fun requiredImageRes(@DrawableRes imageRes: Int): Builder
this.intent = intent
return this
fun optionalTitle(title: String): Builder
this.optionalTitle = title
return this
fun build(): Foo
if(requiredImageRes == -1)
throw IllegalStateException("No image res provided")
return Foo(this.requiredImageRes, this.optionalTitle)
【讨论】:
【参考方案12】:我在 Kotlin 中实现了一个基本的 Builder 模式,代码如下:
data class DialogMessage(
var title: String = "",
var message: String = ""
)
class Builder( context: Context)
private var context: Context = context
private var title: String = ""
private var message: String = ""
fun title( title : String) = apply this.title = title
fun message( message : String ) = apply this.message = message
fun build() = KeyoDialogMessage(
title,
message
)
private lateinit var dialog : Dialog
fun show()
this.dialog= Dialog(context)
.
.
.
dialog.show()
fun hide()
if( this.dialog != null)
this.dialog.dismiss()
最后
Java:
new DialogMessage.Builder( context )
.title("Title")
.message("Message")
.build()
.show();
科特林:
DialogMessage.Builder( context )
.title("Title")
.message("")
.build()
.show()
【讨论】:
【参考方案13】:你可以在 kotlin 中使用可选参数 示例:
fun myFunc(p1: String, p2: Int = -1, p3: Long = -1, p4: String = "default")
System.out.printf("parameter %s %d %d %s\n", p1, p2, p3, p4)
然后
myFunc("a")
myFunc("a", 1)
myFunc("a", 1, 2)
myFunc("a", 1, 2, "b")
【讨论】:
以上是关于如何在 Kotlin 中实现 Builder 模式?的主要内容,如果未能解决你的问题,请参考以下文章
如何在 Kotlin 中实现 OnClickListener 接口? [复制]
如何通过 Interface Builder 在 Objective-C 中实现搜索栏
如何在 KOTLIN 中实现 buttonX.setOnClickListener(this)? [复制]