美文网首页
给人看的Kotlin设计模式——构造者模式

给人看的Kotlin设计模式——构造者模式

作者: 珞泽珈群 | 来源:发表于2020-05-14 18:01 被阅读0次

给人看的Kotlin设计模式目录

概念

构造者模式相对正式的定义:

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.Builderbuild方法返回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,而不是一类产品。

  1. Builder接口,定义了所有产品构建的通用步骤。
  2. 具体的Builder,对于Builder接口的实现,可能包含了针对具体产品的Builder接口之外的方法。
  3. 产品,具体Builder构造的产品,Product1和Product2可能包含了相同的父类(或者接口),也可能没有。
  4. 指挥者,Builder的实际使用者,通常会把常用的构建步骤打包在一起,方便直接构建某种产品,但实际上很少使用。
  5. 客户端。

可以参考文章的第一例子——构造者模式结合类层次结构,那个例子中用到了Builder接口。
文章开头说,建造者模式将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示,对应到上述结构中就是Builder接口与ConcreteBuilder,例如第一个例子中的NyPizza.BuilderCalzone.Builder,我们以相同的构建步骤可以创建出NyPizzaCalzone两种不同的表示。

总结

构造者模式适用情况包括:需要生成的产品对象有复杂的内部结构,这些产品对象通常包含多个成员属性;需要生成的产品对象的属性相互依赖,需要指定其生成顺序;对象的创建过程独立于创建该对象的类;隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同类型的产品。
对于Kotlin而言,构造者模式没有太大的作用,如果想简化表达,增加可读性,可以考虑使用DSL形式的构造者模式。

相关文章

网友评论

      本文标题:给人看的Kotlin设计模式——构造者模式

      本文链接:https://www.haomeiwen.com/subject/yikxohtx.html