概念
构造者模式相对正式的定义:
The builder pattern is an object creation software design pattern with the intentions of finding a solution to the telescoping constructor anti-pattern.
构造者模式是为了解决重叠构造器的问题,telescoping constructor——重叠构造器,很常见,像是:
public class View {
public View(Context context) {
//...
}
public View(Context context, @Nullable AttributeSet attrs) {
//...
}
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
//...
}
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
//...
}
}
重叠构造器被认为是一种“反模式”(就是跟设计模式对着干的意思),它的问题是很明显的,构造器参数较多并且语义不明,容易误用。而构造者模式可以以可读和紧凑的方式获取参数列表,然后验证并实例化对象。
构造者模式对应了Effective Java的第二条:遇到多个构造器参数时要考虑使用构建器,Effective Java指出,构造者模式实际上模拟了具名的可选参数。
核心思想:复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示;参数具名化。
Java中的Builder
构造者模式是一种很常见的设计模式,例如StringBuilder
,这种模式比较简单,我们就不再看具体的例子了,来看看Effective Java中给出的一个较为复杂的构造者模式结合类层次结构的例子:
public abstract class Pizza {
//披萨上的馅料类型
public enum Topping {HAM, MUSHROOM, ONION, PEPPER, SAUSAGE}
final Set<Topping> toppings;
abstract static class Builder<T extends Builder<T>> {
EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
public T addTopping(Topping topping) {
toppings.add(Objects.requireNonNull(topping));
return self();
}
abstract Pizza build();
// 子类返回 "this",模拟self-type
protected abstract T self();
}
Pizza(Builder<?> builder) {
toppings = builder.toppings.clone();
}
}
抽象类Pizza中定义了抽象的Builder,并且这是一个泛型Builder,带有递归类型参数(recursive type parameter),由于Java中缺少self-type,我们只能使用抽象方法self()
要求子类返回"this",模拟self-type,这样就可以在子类中适当地进行方法链接,不需要转换类型。
抽象类定义了抽象的Builder,具体类也有具体的Builder:
public class NyPizza extends Pizza {
public enum Size { SMALL, MEDIUM, LARGE }
private final Size size; //增加size属性
public static class Builder extends Pizza.Builder<Builder> {
private final Size size;
public Builder(Size size) {
this.size = Objects.requireNonNull(size);
}
@Override public NyPizza build() {
return new NyPizza(this);
}
@Override protected Builder self() {
return this;
}
}
private NyPizza(Builder builder) {
super(builder);
size = builder.size;
}
}
public class Calzone extends Pizza {
private final boolean sauceInside; //增加sauceInside属性
public static class Builder extends Pizza.Builder<Builder> {
private boolean sauceInside = false;
public Builder sauceInside() {
sauceInside = true;
return this;
}
@Override public Calzone build() {
return new Calzone(this);
}
@Override protected Builder self() {
return this;
}
}
private Calzone(Builder builder) {
super(builder);
sauceInside = builder.sauceInside;
}
}
两款具体的披萨NyPizza
,Calzone
各自增加了需要的属性,并且实现了对应的具体Builder,每个子类的Builder中的build
方法,都声明返回正确的子类:NyPizza.Builder
的build
方法返回NyPizza
,而Calzone.Builder
中的build
方法返回Calzone
。子类方法声明返回超类中声明的返回类型的子类型,这被称为协变返回类型(covariant return type)。它允许客户端无须转换类型就能使用这些构建器:
NyPizza pizza = new NyPizza.Builder(SMALL)
.addTopping(SAUSAGE).addTopping(ONION).build(); //多次调用addTopping
Calzone calzone = new Calzone.Builder()
.addTopping(HAM).sauceInside().build();
Builder模式十分灵活,可以利用单个Builder构建多个对象 。Builder的参数可以在调用
build
方法来创建对象期间进行调整,也可以随着不同的对象而改变。Builder可以自动填充某些域,例如每次创建对象时自动增加序列号。
Builder模式的确也有它自身的不足。为了创建对象,必须先创建它的构建器。虽然创建这个构建器的开销在实践中可能不那么明显
但是在某些十分注重性能 ,可能就成问题了。
Builder模式还比重叠构造器模式更加冗长,因此它只在有很多参数的时候才使用,比如4个或者更多个参数。但是记住,将来你可能需要添加参数。 如果一开始就使用构造器或者静态工厂,等到类需要多个参数时才添加构造器,就会无法控制,那些过时的构造器或者静态工厂显得十分不协调。因此,通常最好一开始就使用构建器。
简而言之,如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder模式就是一种不错的选择, 特别是当大多数参数都是可选或者类型相同的时候。与使用重叠构造器模式相比,使用Builder模式 的客户端代码将更易于阅读和编写。
Kotlin中的Builder
我们来总结一下在Java中使用构造者模式的优势:
- 参数是显式的。
- 方便为参数设定默认值。
- 可以按任何顺序设置参数。
- 参数更容易修改。
以上这些正对应了Kotlin命名参数+参数默认值,所以在Kotlin中并不太需要构造者模式,或者说,构造者模式已经内化到Kotlin的语言特性中了。例如:
class Dialog(
val title: String,
val text: String? = null,
val onAccept: (() -> Unit)? = null
)
val dialog1 = Dialog(
title = "Some title",
text = "Great dialog",
onAccept = { toast("I was clicked") }
)
val dialog2 = Dialog(
title = "Another dialog",
text = "I have no buttons"
)
val dialog3 = Dialog(title = "Dialog with just a title")
大多数情况下,Kotlin命名可选参数优先于构造者模式,我们不需要专门定义构造者。但这并不是Kotlin建造者模式的唯一新选择,另一种很受欢迎,但是我们自己却很少去实现的对象构建方式是DSL(我们乐于去使用已经定义好的对象构建DSL,自己却懒得或者不会去定义)。来看一个来自于Anko的例子(Anko现在已经废弃了):
alert("Hi, I'm Roy", "Have you tried turning it off and on again?") {
yesButton { toast("Oh…") }
noButton {}
}.show()
即使你不了解Anko库,也可以轻松地看出来上面代码表达的含义,这就是DSL的优势,它更像是一种描述式的语言,可读性强。当有已经定义好的DSL时,我们还是很乐于使用的,毕竟它简洁明了,非常语义化,但是让我们自己去定义一个DSL,往往就无从下手了,其实Kotlin DSL的定义是很套路化的。假设我们需要设置具有多个处理程序的监听器,经典的做法是:
textView.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
// ...
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
// ...
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
// ...
}
})
我们不想如此繁琐,那么可以使用DSL的方式来创建TextWatcher:
class TextWatcherConfiguration : TextWatcher {
private typealias BeforeTextChangedFunction =
(text: String, start: Int, count: Int, after: Int) -> Unit
private typealias OnTextChangedFunction =
(text: String, start: Int, before: Int, count: Int) -> Unit
private typealias AfterTextChangedFunction =
(s: Editable) -> Unit
private var beforeTextChangedCallback: (BeforeTextChangedFunction)? = null
private var onTextChangedCallback: (OnTextChangedFunction)? = null
private var afterTextChangedCallback: (AfterTextChangedFunction)? = null
fun beforeTextChanged(callback: BeforeTextChangedFunction) {
beforeTextChangedCallback = callback
}
fun onTextChanged(callback: OnTextChangedFunction) {
onTextChangedCallback = callback
}
fun afterTextChanged(callback: AfterTextChangedFunction) {
afterTextChangedCallback = callback
}
override fun beforeTextChanged(
s: CharSequence, start: Int, count: Int, after: Int
) {
beforeTextChangedCallback?.invoke(s.toString(), start, count, after)
}
override fun onTextChanged(
s: CharSequence, start: Int, before: Int, count: Int
) {
onTextChangedCallback?.invoke(s.toString(), start, before, count)
}
override fun afterTextChanged(s: Editable) {
afterTextChangedCallback?.invoke(s)
}
}
fun TextView.addOnTextChangedListener(
config: TextWatcherConfiguration.() -> Unit
) {
val listener = TextWatcherConfiguration().apply { config() }
addTextChangedListener(listener)
}
这样一个创建TextWatcher的DSL就定义好了,TextWatcherConfiguration本质上也是一个TextWatcher Builder,定义稍显繁琐,使用起来就简洁很多了:
someTextView.addOnTextChangedListener(
afterTextChanged { s ->
// ...
}
)
Kotlin DSL虽然看上去像是描述语言一样,可读性强,但它本身还是代码,我们可以在其中定义变量,使用if
,for
控制语句等等,并且Kotlin DSL是类型安全的,也就是说我们在其中写代码会有良好的提示和错误检查(与之相对的,想想Groovy下的Gradle)。
如果已经有定义好的Builder(例如来自于Java库),那么在此基础上可以使用Kotlin DSL来简化Builder:
fun Dialog(title: String, init: Dialog.Builder.()->Unit) =
Dialog.Builder(title).apply(init).build()
val dialog1 = Dialog("Some title") {
text = "Great dialog"
setOnAccept { toast("I was clicked") }
}
利用既有的Dialog.Builder
来创建DSL,这样我们就拥有了DSL的优势和非常简洁的声明,这也表明了DSL和Builder模式的共同点。他们有类似的理念,但DSL要更加强大。
结构
构造者模式的经典结构:
经典结构之所以经典是因为它们从不被使用,呸,不对,设计模式的经典结构都是源于GoF的那本经典书籍《设计模式》,其中给出的设计模式的结构都非常完整,但是,现实中我们可能并不需要完整地实现某种设计模式,我们会对它做简化,构造者模式就是这样的,常见的构造者模式的实现几乎都省略了Director和Builder接口的角色,只用一个ConcreteBuilder来实现这两种角色,因为我们通常只是为某个特定的类定义Builder,而不是一类产品。
- Builder接口,定义了所有产品构建的通用步骤。
- 具体的Builder,对于Builder接口的实现,可能包含了针对具体产品的Builder接口之外的方法。
- 产品,具体Builder构造的产品,Product1和Product2可能包含了相同的父类(或者接口),也可能没有。
- 指挥者,Builder的实际使用者,通常会把常用的构建步骤打包在一起,方便直接构建某种产品,但实际上很少使用。
- 客户端。
可以参考文章的第一例子——构造者模式结合类层次结构,那个例子中用到了Builder接口。
文章开头说,建造者模式将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示,对应到上述结构中就是Builder接口与ConcreteBuilder,例如第一个例子中的NyPizza.Builder
和Calzone.Builder
,我们以相同的构建步骤可以创建出NyPizza
和Calzone
两种不同的表示。
总结
构造者模式适用情况包括:需要生成的产品对象有复杂的内部结构,这些产品对象通常包含多个成员属性;需要生成的产品对象的属性相互依赖,需要指定其生成顺序;对象的创建过程独立于创建该对象的类;隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同类型的产品。
对于Kotlin而言,构造者模式没有太大的作用,如果想简化表达,增加可读性,可以考虑使用DSL形式的构造者模式。
网友评论