建造者模式(Builder Pattern,也叫生成器模式)是最常用的设计模式之一,它与之前介绍的『Design Pattern: Singleton』和『Design Pattern: Factory』都属于创建类型的设计模式。

它可以将复杂对象的构建过程抽象出来,使这个抽象过程的不同实现方法可以构造出不同表现(属性)的对象。

在讲 Builder 模式实现之前,先看看之前我们构建复杂对象时是如何编写的。

构造器重载(Telescoping Constructor anti-pattern)

public class Article {
    private String title;
    private String content;
    private String author;
    private Date date;
    public Article(String title, String content) { ... }
    public Article(String title, String content, String author) { ... }
    public Article(String title, String content, Date date) { ... }
    public Article(String title, String content, String author, Date date) { ... }
}

通过在构造器中传递不同数量的参数,从而实现构建不同属性组合的对象。这种方法简单直观,但随着属性增加,构造器的参数组合呈指数级增长,你就很难记住参数的顺序以及在特定情况下你可能需要的特定构造函数,难以维护和理解。

Kotlin 的出现倒是很大程度上解决了这个问题:

data class Article(
    val title: String = "",
    val content: String = "",
    val author: String = "",
    val date: Date = Date()
)

尽管如此,它在扩展性上还是稍有欠缺,比如同一属性不同类型的构造,这时候还是要回到构造函数重载的方案。

data class Article(
    val title: String = "",
    val content: String = "",
    val author: String = "",
    val date: Date = Date()
) {
    constructor(
        title: String = "",
        content: String = "",
        author: String = "",
        millis: Long = System.currentTimeMillis()
    ) : this(title, content, author, Date(millis))
}

Setter

Setter 方法可以很好地解决构造函数膨胀的问题,只需添加对应的重载方法即可:

class Article {
    private var title: String = ""
    private var content: String = ""
    private var author: String = ""
    private var date: Date = Date()
    fun setTitle(title: String) { ... }
    fun setContent(content: String) { ... }
    fun setAuthor(author: String) { ... }
    fun setDate(date: Date) { this.date = date }
    fun setDate(millis: Long) { this.date = Date(millis) }
}

使用 Setter 逐个设置属性的值,灵活性较强,但也可能导致对象在构建过程中处于不完整状态,可变性带来的线程安全性问题,无法保证对象的不变性。

建造者模式

建造者模式通过一个独立的 Builder 类负责构建对象,可以确保对象在构建时处于合法状态。

建造者模式包含如下角色:

  • Builder:抽象建造者
  • ConcreteBuilder:具体建造者
  • Director:指挥者
  • Product:产品角色

下面是一个简单的示例:

/** "Product" */
data class Pizza(var dough: String = "", var sauce: String = "", var topping: String = "")

/** "Abstract Builder" */
abstract class PizzaBuilder {
    lateinit var pizza: Pizza
        private set
    fun createPizza() { pizza = Pizza() }
    abstract fun buildDough()
    abstract fun buildSauce()
    abstract fun buildTopping()
}

/** "Concrete Builder" */
class HawaiianPizzaBuilder : PizzaBuilder() {
    override fun buildDough() {
        pizza.dough = "cross"
    }
    override fun buildSauce() {
        pizza.sauce = "mild"
    }
    override fun buildTopping() {
        pizza.topping = "ham + pineapple"
    }
}

/** "Concrete Builder" */
class SpicyPizzaBuilder : PizzaBuilder() {
    override fun buildDough() {
        pizza.dough = "pan baked"
    }
    override fun buildSauce() {
        pizza.sauce = "hot"
    }
    override fun buildTopping() {
        pizza.topping = "pepperoni + salami"
    }
}

/** "Director" */
class Chef {
    lateinit var pizzaBuilder: PizzaBuilder
    fun getPizza() = pizzaBuilder.pizza
    fun constructPizza() {
        pizzaBuilder.createPizza()
        pizzaBuilder.buildDough()
        pizzaBuilder.buildSauce()
        pizzaBuilder.buildTopping()
    }
}

fun main() {
    val chef = Chef()
    val hawaiianBuilder = HawaiianPizzaBuilder()
    val spicyPizzaBuilder = SpicyPizzaBuilder()
    chef.pizzaBuilder = hawaiianBuilder
    chef.constructPizza()
    val pizza = chef.getPizza()
}

这里构建了一个厨师做披萨了例子,Chef 并不会直接构建 Pizza 对象,因为这个过程很容易出错,而是根据菜谱,也就是 Builder 实例来制作,PizzaBuilder 是个抽象类,由子类来实现具体的制作过程中所需的材料,Chef 无须关心做这个披萨需要菠萝还是火腿,这个由 PizzaBuilder 构建,Chef 拿到菜谱,按照上面的步骤执行即可。

以上这个就是建造者模式比较官方的例子。

抽象的引入似乎很容易会与之前介绍的工厂模式混淆,工厂模式的目的是实现多态性,而建造者模式的目的是找到一种解决伸缩构造器反模式的方法,它使用一个构造器,逐步接收每个初始化参数,然后一次性返回所构造的对象。

简单来说,两者都可以创建复杂的对象,主要区别是工厂模式着重于多个产品对象,且产品对象是立即返回的,而建造者模式着重于一步步构造一个复杂对象,产品在最后的一步返回。

另一方面,虽然利用建造者模式可以创建出不同类型的产品,但是如果产品之间的差异巨大,则需要编写多个建造者类才能实现,如果这时结合工厂模式会是一个更好的选择。

为了更好地理解建造者模式,下面是一个对于 Android 开发者来说更常见的例子,OkHttp 网络请求库内部就大量应用了建造者模式,以创建 Request 为例:

object HttpUtils {
    private val client = OkHttpClient()
    fun doRequest(url: String) {
        val request = Request.Builder().url(url).build()
        client.newCall(request).enqueue(object : Callback {
            ...
        })
    }
}

Request 的构造方法一共 5 个参数,还有其他成员变量,我们通过它的静态内部类 Builder 按需设置它的 urlmethod 等参数,最后调用 build() 方法,Builder 会自动帮我们构建 Request 对象。

可以简单看看 Request 的源码:

class Request internal constructor(
    @get:JvmName("url") val url: HttpUrl,
    @get:JvmName("method") val method: String,
    @get:JvmName("headers") val headers: Headers,
    @get:JvmName("body") val body: RequestBody?,
    internal val tags: Map<Class<*>, Any>
) {
    ...
    open class Builder {
        internal var url: HttpUrl? = null
        internal var method: String
        internal var headers: Headers.Builder
        internal var body: RequestBody? = null
        internal var tags: MutableMap<Class<*>, Any> = mutableMapOf()
        ...
        constructor() {
            this.method = "GET"
            this.headers = Headers.Builder()
        }
        open fun url(url: HttpUrl): Builder = apply {
            this.url = url
        }
        open fun url(url: String): Builder {
            val finalUrl: String = when {
                url.startsWith("ws:", ignoreCase = true) -> {
                    "http:${url.substring(3)}"
                }
                url.startsWith("wss:", ignoreCase = true) -> {
                    "https:${url.substring(4)}"
                }
                else -> url
            }
            return url(finalUrl.toHttpUrl())
        }
        open fun build(): Request {
            return Request(
                checkNotNull(url) { "url == null" },
                method,
                headers.build(),
                body,
                tags.toImmutableMap()
            )
        }
    }
}

我们设置的参数经过处理后首先存储在 Builder 中,调用 build() 方法创建 Request 对象时用其构建实例,如果我们没有配置则使用 Builder 中设定的默认值。

在这里,Builder 同时扮演了上文中提到的 Builder、ConcreteBuilder 角色,在 Builder 模式中如果 ConcreteBuilder 只有一个,我们都可以使用这种写法,省略抽象,简化 Builder 模式的设计。

同时,因为 Builder 里的每一个 Setter 方法都返回了 Builder 对象本身,所以我们在调用的时候就可以使用链式写法,更加简洁。

在 Android SDK 中也时常可以见到建造者模式的身影,比如常用的 AlertDialogNotification 等,由于其涉及到 UI 相关的处理逻辑,代码量较大,就不在这里展开了,可自行阅读源码理解。

优缺点

优点

  • 提高可读性和可维护性:通过使用建造者模式,代码的可读性和可维护性得到提高。建造者模式允许开发者在代码中清晰地看到对象的构建过程,从而更容易理解和修改代码。此外,可以使用链式调用,代码的组织结构更加清晰,易于阅读和编写。
  • 增强对象的不可变性:建造者模式通常与不可变对象(Immutable Objects)一起使用。通过使用建造者模式,可以在对象创建过程中设置所有必要的属性,并在对象构建完成后将其设置为不可变,从而确保对象的一致性和线程安全性,这是一种保护机制,也是建造者模式的特性。
  • 处理可选参数:某些对象可能具有许多可选参数,而不是所有参数都需要在每次创建对象时提供。使用建造者模式,可以通过提供一些设置方法来设置可选参数,而不是在构造函数中使用大量的参数。这使得代码更加简洁,避免了长参数列表的问题。
  • 更好的扩展性:可以通过扩展 Builder 类来支持新的构建步骤,或者通过引入不同的具体 Builder 类来构建不同的对象变种。

缺点

  • 代码量增加:使用建造者模式通常会引入额外的代码,包括 Builder 类本身以及目标类中的 Setter 方法。
  • 可能导致过多的类:如果每个类都需要一个独立的建造者,可能会导致类的数量激增,增加了类的管理和维护的复杂性。
  • 对象状态分散:在建造者模式中,对象的状态可能分散在构造器和目标类中,这使得对象的状态分散,可能导致维护和修改代码更加困难。