阅读 428

建造者模式(Builder Pattern)

什么是建造者模式

定义

将一个复杂对象的构建与它的表示分离,使得同样的构造过程可以创建不同的表示。

即逐步建立由多个部件组成的对象,每次建立中各部件对外接口一致,但内部实现功能可以不一样,相同的构建过程可以创建不同的对象。

特点

适用于流程固定(顺序不一定固定),但建造的目标不同的场景。例如购买电脑,不同人对电脑有不同的需求,办公向的,游戏向的。但都是由机箱、主板、显卡、电源等组成。又如建造房子,不同房子结构不同,但都有地基、墙、地板、门窗等等。又如软件开发,产品经理提出功能需求,技术领导拆分任务,指定具体程序员完成。

适用范例

适用于构建许多字段和嵌套对象组合成的复杂对象。

Lots of subclasses create another problem

例如,假设要建造上图中的 House 。建简单的房子,只需要建造墙和地板,安装门窗,并建造一个屋顶。 但是,如果你想要建造一个更大,更明亮的房子,后院和其他设施(如加热系统,管道和电线)怎么办?

最简单的解决方案是扩展基类 House 并创建一组子类来覆盖参数的所有组合。但它会产生很多的子类。 任何新参数(例如门廊样式)都需要增加新的子类。

还有另一种方法不涉及子类。在基类 House 中创建一个巨型构造函数,其中包含控制 house 对象的所有可能参数。这种方法虽然确实消除了对子类的需求,但它产生了另一个问题。

The telescopic constructor

那就是产生了很多未使用的参数,令构造函数变得很难看。如不是所有房子都有雕像和泳池。

建造者模式将对象的构造过程拆分,并由专门的 Builder 对象来构造对象。

Applying the Builder pattern

该模式将对象构造过程拆分成一组步骤(buildWalls,buildDoor等)。要创建对象,需要在 Builder 对象中执行相应的步骤,并只调用需要的步骤即可,不需要调用所有步骤。

当需要构建产品的各种表示时,某些建造步骤可能需要不同的实现。 例如,小屋的墙壁可以用木头建造,但城堡的墙壁必须用石头建造。

在这种情况下,可以创建多个不同的 Builder 实例类,这些 Builder 以不同的方式实现同一组构建步骤。 然后,可以在构造过程中使用这些 Builder 来生成不同类型的对象。

img

例如,假设一个 Builder ,用木头和玻璃建造一切,第二个用石头和铁建造一切,第三个用金和钻石制造一切。 通过调用相同的步骤,可以获得第一个建筑师的常规房屋,第二个建筑物的小城堡和第三个建筑物的宫殿。

为了更好的构建复杂对象,也可以创建 Director 类,将一系列建造步骤封装到其中,通过调用它来按顺序执行一系列建造步骤。它并不是必须的,但是将常用的建造步骤封装可以更好在其他地方复用。

结构

角色 类别 说明
Builder 抽象的建造者 接口或抽象类,将建造的具体过程交由其子类实现,便于扩展。但所有建造步骤和返回产品函数均在此声明。
ConcreteBuilder 具体的建造者 可以有多个,实现 Builder 中的所有建造步骤。不同建造者的实现方式可以不同。
Product 具体的产品类 要被建造的较为复杂的对象。
Director 导演者 定义调用建造步骤的顺序。一般不与产品类发生依赖关系,与建造者类直接交互,通常封装可能被经常使用的一系列建造步骤。

UML图

Structure of the Builder design pattern

在整个过程中:

  1. 用户不需了解建造过程和细节
  2. 用户只需给出复杂对象的的类型即可创建对象
  3. 建造者按流程一步步创建出复杂对象

示例

完整版 - SettingItemView

settingItemViewComponent

如上图所示,设置选项由很多个部分组成,但每次实际使用时,不需要同时启用所有部分,而是根据使用情况不同,组合不同部分,同时一些部分还有先后顺序,如点击事件需先添加了右箭头才能添加。此类复杂对象可以使用建造者模式创建(此处仅用于演示建造者模式。 android 中实际使用时,为其添加自定义 attr 属性,在 xml 中直接设置更方便,并且实际应用建造模式时并不需要严格创建四个部分,除 Product 外其余三个部分经常混合使用)。

  1. Product — SettingItemView

    class SettingItemView(context: Context) : LinearLayout(context) {
    		// ... 省略构建函数
        fun addTitle(title: String) {
            settingTitleView.visibility = View.VISIBLE
            settingTitleView.text = title
        }
    
        fun addRightArrow(isShow: Boolean) {
            settingRightArrow.visibility = View.VISIBLE
            settingRightArrow.visibility = if (isShow) View.VISIBLE else View.GONE
        }
    
        fun addOnClickListener(listener: View.OnClickListener) {
            if (if (settingRightArrow.visibility != View.VISIBLE) {
                throw IllegalStateException("add click listener should after adding right arrow")
            }
            setOnClickListener(listener)
        }
      	// ... 省略设置函数
    }
    复制代码
  2. Builder

    interface Builder {
      
        fun reset()
    
        fun addTitle(title: String)
    
        fun addRightArrow(isShow: Boolean)
    
        fun addOnClickListener(listener: View.OnClickListener)
    
        fun create(): SettingItemView
    }
    复制代码
  3. ConcreteBuilder

    class ConcreteBuilder(private val context: Context) : Builder {
    
        private var settingItemView = SettingItemView(context)
      
        override fun reset() {
            settingItemView = SettingItemView(context)
        }
    
        override fun addTitle(title: String) {
            settingItemView.addTitle(title)
        }
    
        override fun addRightArrow(isShow: Boolean) {
            settingItemView.addRightArrow(isShow)
        }
    
        override fun addOnClickListener(listener: View.OnClickListener) {
            settingItemView.addOnClickListener(listener)
        }
    
        override fun create(): SettingItemView {
            return settingItemView
        }
    }
    复制代码

    Builder 构建时另一种常用方式是将所有参数记录下来,在创建时再统一构建并检查错误。

  4. Director

    class Director(private val builder: Builder) {
    
        fun construct() {
            builder.addTitle("账号与安全")
            builder.addRightArrow(true)
            builder.addOnClickListener(View.OnClickListener {
                Toast.makeText(it.context, "点击", Toast.LENGTH_SHORT).show()
            })
        }
    }
    复制代码

    Director 通常只负责调用 Buidler 执行操作,不直接返回产品。

  5. 调用

    val builder = ConcreteBuilder(context)
    val director = Director(builder)
    val settingItemView = builder.create()
    复制代码

将生成的 SettingItemView 对象加载到窗口中去后如下图所示:

settingItemView.png

简化版 - AlertDialog

在实际应用中,采用建造模式构建复杂对象时,通常会对建造模式进行一定简化,大致简化如下:

  1. 直接使用建造者模式创建对象,所以不用再定义一个抽象建造类接口,直接提供一个具体的建造类即可。
  2. 创建一个复杂对象时,可能有很多种不同的选择和步骤,可以去掉导演者,把导演者的功能和客户的功能结合起来,此时客户就相当于导演者,它来指导建造器去构建需要的复杂对象。
public class AlertDialog extends Dialog implements DialogInterface {
    // ... 省略若干构造方法
    protected AlertDialog(Context context, @StyleRes int themeResId) {
        this(context, themeResId, true);
    }
    // ... 省略若干设置属性方法
    public void setView(View view) {
        mAlert.setView(view);
    }
    // 建造者
    public static class Builder {
        // ... 省略
        public Builder setTitle(CharSequence title) {
            P.mTitle = title;
            return this;
        }
        // 建造者中提供创建目标对象的方法
        public AlertDialog create() {
            // Context has already been wrapped with the appropriate theme.
            final AlertDialog dialog = new AlertDialog(P.mContext, 0, false);
            P.apply(dialog.mAlert);
            dialog.setCancelable(P.mCancelable);
            if (P.mCancelable) {
                dialog.setCanceledOnTouchOutside(true);
            }
            dialog.setOnCancelListener(P.mOnCancelListener);
            dialog.setOnDismissListener(P.mOnDismissListener);
            if (P.mOnKeyListener != null) {
               dialog.setOnKeyListener(P.mOnKeyListener);
            }
            return dialog;
        }
    }
}
复制代码

使用建造模式:

val builder = AlertDialog.Builder(context)
builder.setTitle("问题:")
    .setMessage("请问你满十八岁了吗?")
    .setIcon(R.mipmap.ic_launcher_round)
    .setCancelable(true)
val dialog = builder.create()
复制代码

AlertDialog 的建造者模式 AlertDialog.Builder 同时扮演了 BuilderConcreateBuilderDirector 三个角色。

优缺点

优点

  1. 创建产品的步骤和产品本身分离,相同中的创建过程可以创建出不同的产品。
  2. 允许对象通过多个步骤创建,可以改变过程。
  3. 向客户隐藏具体实现。
  4. 建造者相对独立,可以方便的替换或增加新的。

缺点

  1. 产品必须有共同点,范围有限制。
  2. 如果产品内部发生改变,多个建造者都需要修改,成本大。
  3. 采用生成器模式创建对象的客户,需要具备更多的领域知识。

使用场景

  1. 对象具有复杂的内部结构
  2. 想将复杂对象的创建和使用分离
  3. 要生产的对象属性相互依赖,需要指定生成顺序
  4. 一些基本部分不会变,而其组合经常变化的时候

建造者模式 VS 工厂模式

相似

都属于创建模式

不同

  1. 意图不同

    工厂模式关注的是对象整体,不关注对象的组成部分,建造者模式关注对象组成部分的创建过程。

  2. 粒度不同

    工厂模式创建的产品性质相对单一,建造者模式创建的是复合产品,由复杂部分组成,部分不同构成的产品也不同。

Article by Wuhb