Builder 模式大家想必也都知道。但是应该在什么时候使用呢?如果满足以下条件,那么应该使用:
那么 Java 里常见的 Builder 模式再简单不过了(以下例子做了缩短,同时用了 Kotlin 来表达):
class AppRequest { private var uid: String? = null private var appId: String? = null constructor(uid: String, appId: String) { this.uid = uid this.appId = appId } class Builder { private var uid: String? = null private var appId: String? = null fun setUid(uid: String?): Builder { uid ?: return this this.uid = uid return this } fun setName(appId: String?): Builder { appId ?: return this this.appId = appId return this } fun build(): AppRequest { requireNotNull(uid) requireNotNull(appId) return AppRequest(uid!!, appId!!) } } }
写起来很繁琐,在 Kotlin 里可以用 data class 取代,同时如果可以,尽量多使用默认参数:
data class Request( val uid: String, val appId: String = DEFAULT_APP_ID )
当然如果你还需要做好对 Java 的兼容,那么 @JvmOverloads 还是有必要的。
使用 data class,又或者在主构造函数里就定义属性的话,如果调用方是使用 Kotlin 的话,那么调用起来会比较舒服:
fun foo() { val request = Request(uid = “fake_uid”) }
当然,如果你的这个对象本来的属性是 mutable 的,又或者不得不做成 mutable(比如我司的 ParcelablePlease 插件,帮助实现 Parcelable 接口的,就要求 field 必须至少是包可见,对 Kotlin 来说就是需要 public 了),那么我个人比较倾向添加这一种构造函数:
class Request { var uid: String? = null var appid: String = DEFAULT_APP_ID constructor() constructor(applier: Request.() -> Unit) { applier() } }
当然代价就是:
public interface Function1<in P1**,**out R> : Function<R>
对象。如果此目标对象需要频繁创建,那么此为一个不怎么好的方法。 最 “环保” 的做法自然是,调用方直接使用:
fun foo() { val request = Request().apply { uid = “fake_uid” } }
当然 Kotlin 这么做算是比较方便,如果确认了目标调用方还可能是使用 Java,那么还是最好做成 fluent 式风格。
前面说过 Data Class 是一个比较好的做法,但是也忽略了一些问题,就是二进制的兼容性。以下内容总结自 Jake 大神的文章,欢迎直接到 原文 阅读。
如果你经常对外输出 API,那么对一个公开的对象,他的属性不可能一成不变的。如果你在使用 data class,那么就要小心了,先来看看如果你把一个类定义为 data class,Kotlin 编译器会帮你做什么:
二进制兼容的意思是,上层使用提供的 Public 方法/对象,在你底层修改代码后,上层是不需要重新编译的就能正常运行的。那么看看在这里有没可能会出现问题?
首先是 Deconstruct 就直接出问题了。对于这么一个类:
data class Person(val uid: String, val name: String = “no_name”)
调用方是这么写的:
fun foo() { val person = Person(uid = “fake_uid”, name = “Tony”) val (uid, name) = person }
那么假如说需要在 uid 和 name 之间新增一个 long 类型的属性,会怎样?
data class Person(val uid: String, val birthday: Long = 0L, val name: String = “no_name”)
如果只是执行上面的例子,那么看起来还一切正常:
fun foo() { val person = Person(uid = “fake_uid”, name = “Tony”) val (uid, name: String) = person }
但是你会发现, name
这个不再是 Person
里的 name
了,会变成 Long
类型了。因此如果你在下面会继续用到这个 name
,那么就会出错了。当然,编译器报错那还好,怕的是下面有一个误写的 toString()
,然后直接当字符串处理了,然后 Bug 就出来了...
就以上问题的解决方法是:
在 data class 主构造的末尾通过 append 的方式添加新的属性。
当然,如果你还使用 Copy
方法,那么你会发现,Kotlin 编译出来的 Java 等效代码里,也有一个 Copy
的方法,方法接受的参数包含了所有属性
public static Person copy$default(Person var0, String var1, long var2, String var4, int var5, Object var6)
那么对调用方来说,一旦调用了 Copy
,那么你的底层对象在做属性变更的时候,这个 Copy
方法自然就不存在了,那么将会导致运行时发生 NoSuchMethodError
异常。
因此,Jake 提出可以这么写:
总结出来就是,抄自 Jake 的 Blog 并加上 componentN 的方法:
class Person private constructor( val name: String, val nickname: String?, val age: Int ) { @JvmSynthetic operator fun component1(): String { return name } @JvmSynthetic operator fun component2(): String? { return nickname } @JvmSynthetic operator fun component3(): Int { return age } override fun toString() = "Person(name=$name, nickname=$nickname, age=$age)" override fun equals(other: Any?) = other is Person && name == other.name && nickname == other.nickname && age == other.age override fun hashCode() = Objects.hash(name, nickname, age) class Builder { @set:JvmSynthetic // Hide 'void' setter from Java var name: String? = null @set:JvmSynthetic // Hide 'void' setter from Java var nickname: String? = null @set:JvmSynthetic // Hide 'void' setter from Java var age: Int = 0 fun setName(name: String?): Builder = apply { this.name = name } fun setNickname(nickname: String?): Builder = apply { this.nickname = nickname } fun setAge(age: Int): Builder = apply { this.age = age } fun build() = Person(name!!, nickname, age) } }
那么在 Kotlin 里,你可以这么写:
fun foo() { val person = Person { name = “Photon” age = 10 } val (name, _, age) = person }
在 Java 里,可以:
public void foo() { Person person = new Person.Builder() .setName(“John”) .setNickname(“nick”).build(); }