虔诚地向Builder模式道个歉

引子

曾经在博文中发表过一些关于“Builder模式”的错误言论,误导了读者。今天在此虔诚地向Builder模式道个歉。

这个错误言论出现在面试题 | 怎么写一个又好又快的日志库?(一)。在该篇博文中,我如是说:

Kotlin 相较于 Java 的最大优势就是降低复杂度。在库接口设计及内部实现时就要充分发挥 Kotlin 的优势,比如 Kotlin 的世界里已不需要 Builder 模式了。

Builder 模式有如下优势:

  1. 为参数标注语义:在Builder模式中,每个属性的赋值都是一个函数,函数名标注了属性语义。
  2. 可选参数&分批赋值:Builder模式中,除了必选参数,其他参数是可选的,可分批赋值。而直接使用构造函数必须一下子为所有参数赋值。
  3. 增加参数约束条件:可以在参数不符合要求时,抛出异常。

但 Builder 模式也有代价,新增了一个中间类Builder

使用 Kotlin 的命名参数+参数默认值+require()语法,在没有任何副作用的情况下就能实现 Builder 模式:

class Person(
    val name: String,
    //'为以下可选参数设置默认值'
    val gender: Int = 1,
    val age: Int= 0,
    val height: Int = 0,
    val weight: Int = 0
)
//'使用命名参数构建 Person 实例'
val p  = Person(name = “taylor”,gender = 1,weight = 43)

命名参数为每个实参赋予了语义,而且不需要按构造方法中声明的顺序来赋值,可以跳着来。

如果想增加参数约束条件可以调用require()方法:

data class Person(
    // 这个是必选参数
    val name: String,
    val gender: Int = 1,
    val age: Int= 0,
    val height: Int = 0,
    val weight: Int = 0
){
    //'在构造函数被调用的时候执行参数合法检查'
    init {
        require(name.isNotEmpty()){”name cant be empty“}
    }
}

此时如果像下面这样构造 Person,则会抛出异常:

val p = Person(name="",gender = 1)
java.lang.IllegalArgumentException: name cant be empty

本来在 build() 方法中执行的额外初始化逻辑也可以全部写在init代码块中。

上述结论好像在给定的场景中找不出任何毛病。

但我遗漏了一个场景,“构建对象”和“属性赋值”是可以分开进行的。比如下面这个场景。

syntax = "proto3";

message Event {
 int64 time = 1; // 事件生成时间
 string id = 2; // 事件唯一标识符
 string name = 3;// 事件名称
}

message BatchEvent {
  repeated Event event = 1;
}

使用 protobuf 定义了两个结构体,其中Event表示单个事件,而BatchEvent表示批量事件,它持有多个事件。

protobuf对应的java代码会使用建造者模式,这为构建批量事件提供了便利。

假设有一个事件流:

val initEvent = event {
    time = System.currentTimeMillis()
    id = UUID.randomUUID()
    name = "init"
}
val eventFlow = MutableStateFlow(initEvent)

不同类型的事件在 eventFlow 中流动。当遇到 flush 事件时,会将之前堆积的所有事件包装成 BatchEvent 上传网络。

这就是一个属性赋值和构建对象分开进行的场景,即一直为 BatchEvent 对象的 events 属性追加事件,直到遇到了特殊事件时才构建对象:

var builder = BatchEvent.newBuilder()
eventFlow.collect { event ->
    builder.addEvent(event)
    if(event.name == "flush") {
        upload(builder.build())
        builder = BatchEvent.newBuilder()
    }
}

最后再下一个对 Builder 模式完整的总结:

  1. 为参数标注语义:在Builder模式中,每个属性的赋值都是一个函数,函数名标注了属性语义。
  2. 可选参数&分批赋值:Builder模式中,除了必选参数,其他参数是可选的,可分批赋值。而直接使用构造函数必须一下子为所有参数赋值。
  3. 增加参数约束条件:可以在参数不符合要求时,抛出异常。
  4. 方便实现属性赋值和构造对象的分离。

猜你喜欢

转载自juejin.im/post/7249904542866047031