美文网首页
给人看的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