只一篇就够了·设计模式(4) - 建造者模式

建造者模式(Builder Pattern)简化了构建复杂对象的过程,除了简化复杂的对象的构建过程,最核心的思想是化整为零、按需分配。

先说如何简化构建过程,建造者模式就像是Google的模块化手机,通过各个零件的定制来完成一部手机,比如,我可以装两个摄像头,或者把添加多一块电池,只是换个模块就能完成。

化整为零、按需分配说的是如果要实现一个多变的对象,把这个多变对象的整体,分解成一小块一小块的,然后组装起来,还能按照需求决定哪些需要定制,怎么定制,比如,Android或者Ios中常用的DialogNotification等。

类图

类图不是目的,仅仅帮助理解

[图片上传失败...(image-b3bc3-1527174228599)]

IBuilder是一个建造者接口,规范了建造的内容,可以有很多实现,比如,BuilderABuilderB,有一个重点就是化整为零的产品和Builder 之间是组合关系,有了建造者和产品我们就可以通过建造者来定制产品了,这时候Director的作用就是规定构造产品的顺序或者一些固定的其他默认属性,比如构建的时候依赖顺序必须先是A后是B,那么可以通过Director来控制,这里的Direcotr抽象工厂模式有点像。

聚合关系:对象和对象之间是整体部分的关系,部分的生命周期可以超越整体,这是弱关系。
组合关系:对象和对象之间是整体部分的关系,部分的生命周期不能超越整体,这是强关系。

实例

先看看实例的类图。

类图不是目的,仅仅帮助理解

[图片上传失败...(image-473fe6-1527174228599)]
还是以手机为例,规定一个手机产品的创建流程,用于流水线生产,手机的生产需要生产:主板、CPU、内存、屏幕、外壳。现在要实现一部手机,可以这样做:

fun main(args: Array<String>)
{
    val nexus5 = Phone("Google")
    nexus5.cpu = "Google CPU"
    nexus5.ram = "Google RAM"
    nexus5.screen = "Google Screen"
    nexus5.motherboard = "Google Motherboard"
    nexus5.view = "Google Nexus5 View"
    println(nexus5)
}
// Phone
/**
 * 手机
 * Created by Carlton on 2016/11/15.
 */
class Phone(val name: String)
{
    /**
     * cpu
     */
    var cpu: String? = null
    /**
     * 内存
     */
    var ram: String? = null
    /**
     * 屏幕
     */
    var screen: String? = null
    /**
     * 主板
     */
    var motherboard: String? = null
    /**
     * 外观
     */
    var view: String? = null

    override fun toString(): String
    {
        return "Phone(name='$name', cpu=$cpu, ram=$ram, screen=$screen, motherboard=$motherboard, view=$view)"
    }
}

这样,创建了一只Nexus5的手机,如果现在需要一只Nexus6手机怎么做呢?重新创建一个Phone实例,然后给属性赋值。如果Nexus6手机的CPURAMNexus5一样,那赋值的代码就重复了,不方便重用了。如果还需要一个苹果6手机,又得重新去实例化Phone对象,如果再建造一个苹果7,CPU主板都一样,就会重复做很次这些操作,关键问题还不在这里,关键问题是暴露了产品的具体信息,这样产品类就变得极其不稳定,后期修改产品类的时候很难维护,因为很多地方在修改属性,如果使用建造者包装一次,客户端就不知道产品内部的具体信息(只有建造者知道,这样就控制了产品类出现的次数),后面修改产品类的时候就比较轻松。
还有一个问题是如果对构造顺序有严格的要求,比如必须先建主板才能建cpu那么,上面这种方式就不能控制。建造者的实现:

fun main(args: Array<String>)
{
    // 首先创建一个Google手机的建造者创建一个nexus5手机
    val googleBuilder = GoogleBuilder()
    googleBuilder.buildCpu("Google CPU")
    googleBuilder.buildMotherboard("Google Motherboard")
    googleBuilder.buildRam("Google RAM")
    googleBuilder.buildScreen("Google Screen")
    googleBuilder.buildView("Google View")
    val director = Director(googleBuilder)
    val nexus5 = director.build()
    println(nexus5)

    // 现在创建一个nexus6的手机,还是用nexus5的建造者,屏幕和外观不一样
    googleBuilder.buildScreen("Google Big Screen")
    googleBuilder.buildView("Google Big View")
    println(Director(googleBuilder).build())
}

// 打印
Phone(name='Google', cpu=Google CPU, ram=Google RAM, screen=Google Screen, motherboard=Google Motherboard, view=Google View)
Phone(name='Google', cpu=Google CPU, ram=Google RAM, screen=Google Big Screen, motherboard=Google Motherboard, view=Google Big View)

/**
 * 手机Builder
 * Created by Carlton on 2016/11/15.
 */
interface IPhoneBuilder
{
    /**
     * 定制CPU
     */
    fun buildCpu(cpu: String?)

    /**
     * 定制内容
     */
    fun buildRam(ram: String?)

    /**
     * 定制屏幕
     */
    fun buildScreen(screen: String?)

    /**
     * 定制主板
     */
    fun buildMotherboard(motherboard: String?)

    /**
     * 定制视图
     */
    fun buildView(view: String?)

    /**
     * 创建
     */
    fun create(): Phone
}

// Google手机建造者
/**
 * 谷歌手机建造者
 * Created by Carlton on 2016/11/15.
 */
class GoogleBuilder : IPhoneBuilder
{
    override fun create(): Phone
    {
        return phone
    }

    val phone = Phone("Google")
    override fun buildCpu(cpu: String?)
    {
        phone.cpu = cpu
    }

    override fun buildRam(ram: String?)
    {
        phone.ram = ram
    }

    override fun buildScreen(screen: String?)
    {
        phone.screen = screen
    }

    override fun buildMotherboard(motherboard: String?)
    {
        phone.motherboard = motherboard
    }

    override fun buildView(view: String?)
    {
        phone.view = view
    }
}
// 组装者
/**
 * 组装者
 * Created by Carlton on 2016/11/16.
 */
class Director(val builder: IPhoneBuilder)
{
    /**
     * 顺序建造
     */
    fun build(cpu: String, ram: String, motherboard: String, screen: String, view: String): Phone
    {
        builder.buildMotherboard(motherboard)
        builder.buildCpu(cpu)
        builder.buildRam(ram)
        builder.buildScreen(screen)
        builder.buildView(view)
        return builder.create()
    }

    /**
     * 建造
     */
    fun build(): Phone
    {
       return builder.create()
    }
}

首先,客户端创建了一个Google手机的建造者,并且分别建造了各个部件,然后拿到组装者去组装,组装的时候就可以按照一定的顺序来组装,或者在组装的时候做一些其他事情,接来下让建造者修改了其中两个部件屏幕和外观,然后造了一个新手机。这样做可以轻易的替换建造者,而其他部分代码不用修改来控制建造过程。
总结一下,建造者(IBuilder)可以隐藏具体的产品建造过程,产品的消费者只需要拿到完整的产品,组装者(Director)可以控制产品组装的流程,具体的产品的创造和实例化客户端根本不关心。建造者也提供了很强的扩展性,通过替换建造者或者修改某一个建造者,就能在背后影响产品的创造过程,而客户端也就是消费者并不知道,建造者把业务需求表现的差异化实现封装到了IBuilderDirector

和工厂模式的区别

和工厂模式一样都是输入创建类型的设计模式,封装创建过程给消费者,从类图上可以看出来和抽象工厂模式很像,但是,之前说过,类图只是参考,学习设计模式主要是学习其思路,在思路上抽象工厂模式是直接创建一个产品,及时的就把产品创造出来了,而建造者模式是先准备和定制产品属性,最后通过build()或者create()来创建一个产品。建造者的创建过程可以由客户端来控制,在创建过程上比抽象工厂模式更加灵活,在概念上抽象工厂模式创建的是一个产品族,是一类整体,建造者模式中产品过程则是独立的个体。

建造者的变种

建造者的核心在想在于创建产品,由很小的一些块组成整体产品。所以一般情况下不需要使用标准的建造者格式,大多数时候建造顺序不重要,这样只需要一个Builder类,连接口都可以不使用,这种情况在很多地方都应用,举个例子:

Java中的java.util.Calendar.Builder,是一个针对日历实例的建造者

[图片上传失败...(image-d31cc6-1527174228599)]
可以看到这里面有很多set方法,这些方法就是在定制这个产品,你会发现不需要有任何的顺序或者必须要调用,一个产品匹配一个建造者,主要作用是简化了实例过程,因为需要设置的属性太多了!最后使用build方法生产一个Calendar实例,这是典型的使用方式。

设计模式不要局限于形式,而在于思想

几个其他的例子

用几个简单的例子来加深一下理解。

StringBuilder

Java中如果要对String进行操作尽量使用StringBuilder,原因是String的连接等操作会产生新的实例,它是一个不可变的对象,比如,String str = "abc" + "bcd";这里内存中会产生3个对象,"abc"、"bcd"都是一个String对象,然后连接后把引用给到str
那么,StringBuilder在这里有什么作用呢?StringBuilder里面是一个char数组,如果用StringBuilder来做应该是这个样子:

val stringBuilder  = StringBuilder()
stringBuilder.append("abc")
stringBuilder.append("bcd")
val str = stringBuilder.toString()

同样的都是拼接"acb"和"bcd"如果使用StringBuilder的话我们只会在toString()的时候创建一个String,不是每一次都去创建好一个产品,然后做操作,这样就提高了性能:

@Override
public String toString() {
    // Create a copy, don't share the array
    return new String(value, 0, count);
}

这里的toString()build()、create()是一个意思。用建造者模式,我们可以对产品各个情况先组建好,这里就是对字符各种操作先操作完成,最后一次输出完整的字符串对象。

GsonBuilder

Gson我相信都用过,是一个Json转对象,对象转Json的库,和FastJson一样,前者是Google的,后者是阿里的。先看一段代码:

……
public final class GsonBuilder 
{
    private Excluder excluder;
    private LongSerializationPolicy longSerializationPolicy;
    private FieldNamingStrategy fieldNamingPolicy;
    private final Map<Type, InstanceCreator<?>> instanceCreators;
    private final List<TypeAdapterFactory> factories;
    private final List<TypeAdapterFactory> hierarchyFactories;
    private boolean serializeNulls;
    private String datePattern;
    private int dateStyle;
    private int timeStyle;
    private boolean complexMapKeySerialization;
    private boolean serializeSpecialFloatingPointValues;
    private boolean escapeHtmlChars;
    private boolean prettyPrinting;
    private boolean generateNonExecutableJson;
……

这里有很多很多很多的属性,这些属性关系到对Json的解析和处理方式,我们不可能每次解析Json的时候都去赋值这么多属性,所以看看使用建造者模式如何规避这个问题,GsonBuilder在实例化的时候预先了一些默认值:

public GsonBuilder() 
{
    this.excluder = Excluder.DEFAULT;
    this.longSerializationPolicy = LongSerializationPolicy.DEFAULT;
    this.fieldNamingPolicy = FieldNamingPolicy.IDENTITY;
    this.instanceCreators = new HashMap();
    this.factories = new ArrayList();
    this.hierarchyFactories = new ArrayList();
    this.dateStyle = 2;
    this.timeStyle = 2;
    this.escapeHtmlChars = true;
}

然后也提供了一些方法来修改部分属性,也就是建造者方法,这样可以通过GsonBuilder来建造一个Gson实例,而不用过多的去关注建造过程。当所有的属性都准备完成后,一次性输出产品:

public Gson create()
{
    ArrayList factories = new ArrayList();
    factories.addAll(this.factories);
    Collections.reverse(factories);
    factories.addAll(this.hierarchyFactories);
    this.addTypeAdaptersForDate(this.datePattern, this.dateStyle, this.timeStyle, factories);
    return new Gson(this.excluder, this.fieldNamingPolicy, this.instanceCreators, this.serializeNulls, this.complexMapKeySerialization, this.generateNonExecutableJson, this.escapeHtmlChars, this.prettyPrinting, this.serializeSpecialFloatingPointValues, this.longSerializationPolicy, factories);
}

Android中也有很多建造者的应用比如:android.support.v4.app.NotificationCompat.Builder、android.support.v7.app.AlertDialog.Builder,为什么使用建造者,建造者又有哪些缺点,通过这些实例自己能够去理解才是最重要的。

总结

建造者模式和抽象工厂类似都是封装了产品的建造过程,区别是建造者模式是构建完后一次性输出完整的产品,抽象工厂创建实例的时候,直接就输出了完成的产品,相比之下,建造者可以定制和控制构建过程,建造者也简化了产品的创建过程。

😊查看更多😊

不登高山,不知天之高也;不临深溪,不知地之厚也
感谢指点、交流、喜欢

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容