设计模式篇(二)——建造者模式

一、简介

建造者模式(Builder Pattern) 也比较常见,该模式与单例模式同属于创建型模式,当我们需要构建一个复杂的对象,就可以利用该模式,达到精简优化的效果。

定义:建造者模式将复杂对象的构造与其表示分离,以便同一个构造过程可以创建不同的表示。

使用场景:构建复杂的对象,在 Android 中有 Notification 和 Dialog 等。

实现要素:

  1. 将所有的构造相关的方法,置于 Builder 中。
  2. 通过建造者模式获得的对象,你只能使用它,而不能修改其属性。

通俗的讲:一个类的构造,有 settergetter ,使这两种类型的方法分开,创建一个 Builder 角色,承担类的 setter ,而 getter 则还是保留与原本类中。

二、实现

我们只需要关注建造者模式的精髓,在各种项目中,你完全可以发挥自己的小技巧,来实现各种不同的变种,事实上其他的设计模式也是类似的。

1、角色介绍
  • Product 需要构建的具体类。
  • Builder<Abstract> 这是个抽象基类,可以是抽象类也可以是接口,用于定义 Product 的构建方法。
  • Concrete BuilderBuilder 的具体实现类,你可以创建多个实现类,来达到创建不同表示的目的。
  • Director 通过该类,你可以实现具体某一个 Concrete Builder,从而来得到一个具体的 Product
2、普通类实现
class Pc {

    private var mName: String? = null
    private var mCpu: String? = null
    private var mRomSize: Int? = null

    fun setName(name: String) {
        this.mName = name
    }

    fun setCpu(cpu: String) {
        this.mCpu = cpu
    }

    fun setRomSize(romSize: Int) {
        this.mRomSize = romSize
    }

    fun getName():String{
        return mName?:"没名字"
    }

    fun getCpu():String{
        return mCpu?:"没名字"
    }

    fun getRomSize():Int{
        return mRomSize?:0
    }
}


fun main() {
    val pc = Pc()
    pc.setName("Surface Book")
    pc.setCpu("Intel i7")
    pc.setRomSize(512)

    print(pc.getName())
}

如代码所见,我们构建一个 Pc 对象,需要直接操控 Pc 对象,通过其方法。

3、通过建造者模式实现

我们先看一个UML图,然后再结合代码,会更清楚整体的框架。
UML

//这是 Product
class Pc {

    private var mName: String? = null
    private var mCpu: String? = null
    private var mRomSize: Int? = null

    fun setName(name: String) {
        this.mName = name
    }

    fun setCpu(cpu: String) {
        this.mCpu = cpu
    }

    fun setRomSize(romSize: Int) {
        this.mRomSize = romSize
    }

    fun getName():String{
        return mName?:"没名字"
    }

    fun getCpu():String{
        return mCpu?:"没名字"
    }

    fun getRomSize():Int{
        return mRomSize?:0
    }
}

//这是 Builder 接口
interface PcBuilder{
    fun setName()
    fun setCpu()
    fun setRomSize()
    fun createPc():Pc
}

//这是具体的 Builder
class SurfaceBookBuilder: PcBuilder{

    private var pc:Pc

    init {
        pc = Pc()
    }
    
    override fun setName() {
        pc.setName("Surface Book")
    }

    override fun setCpu() {
        pc.setCpu("Intel i7")
    }

    override fun setRomSize() {
        pc.setRomSize(512)
    }

    override fun createPc(): Pc {
        return pc
    }
}

//这是具体的 Builder
class MacBookBuilder : PcBuilder{

    private var pc:Pc

    init {
        pc = Pc()
    }

    override fun setName() {
        pc.setName("MacBook")
    }

    override fun setCpu() {
        pc.setCpu("Intel i7")
    }

    override fun setRomSize() {
        pc.setRomSize(1024)
    }

    override fun createPc(): Pc {
        return pc
    }
}

//这是组装类
class PcDirector(private val mBuilder: PcBuilder){

    fun getPc(): Pc {
        constructPc()
        return mBuilder.createPc()
    }

    private fun constructPc(){
        mBuilder.setName()
        mBuilder.setCpu()
        mBuilder.setRomSize()
    }
}


fun main() {
    //创建一个 SurfaceBook
    val pcBuilder = SurfaceBookBuilder()
    val surfaceBook = PcDirector(pcBuilder).getPc()
	//创建一个 MacBook
	val pcBuilder2 = MacBookBuilder()
    val macBook= PcDirector(pcBuilder2).getPc()
}

问题分析:虽然我们都创建了各个角色,也最终实现了效果,
但是,回到建造者模式的本质:将构建与表示分离。我们真的做到了分离吗?
答案是:No。因为我们还是能够通过创建好的对象来继续调用其 setter,例如surfaceBook.setName(),所以这是一个失败的建造者模式

4、修正

为了使得我们不能再继续调用surfaceBook.setName(),那该如何解决这个问题呢?
答:将 Product 类的 setter 方法私有化

但是私有化后,我们的 Builder 类却无法访问其 setter 方法,那又怎么办呢?
答:使 Builder 类成为Product 的内部类。

修改后的代码:

interface PcBuilder{
    fun setName()
    fun setCpu()
    fun setRomSize()
    fun createPc():Pc
}

class Pc {

    private var mName: String? = null
    private var mCpu: String? = null
    private var mRomSize: Int? = null
	
    private fun setName(name: String) {
        this.mName = name
    }

    private fun setCpu(cpu: String) {
        this.mCpu = cpu
    }

    private fun setRomSize(romSize: Int) {
        this.mRomSize = romSize
    }

    fun getName():String{
        return mName?:"没名字"
    }

    fun getCpu():String{
        return mCpu?:"没名字"
    }

    fun getRomSize():Int{
        return mRomSize?:0
    }

    class SurfaceBookBuilder : PcBuilder{

        private var pc:Pc

        init {
            pc = Pc()
        }

        override fun setName() {
            pc.setName("Surface Book")
        }

        override fun setCpu() {
            pc.setCpu("Intel i7")
        }

        override fun setRomSize() {
            pc.setRomSize(512)
        }

        override fun createPc(): Pc {
            return pc
        }
    }

    class MacBookBuilder : PcBuilder{

        private var pc:Pc

        init {
            pc = Pc()
        }

        override fun setName() {
            pc.setName("MacBook Air 2020")
        }

        override fun setCpu() {
            pc.setCpu("Intel i7")
        }

        override fun setRomSize() {
            pc.setRomSize(1024)
        }

        override fun createPc(): Pc {
            return pc
        }
    }
}


class PcDirector(private val mBuilder: PcBuilder){

    fun getPc(): Pc {
        constructPc()
        return mBuilder.createPc()
    }

    private fun constructPc(){
        mBuilder.setName()
        mBuilder.setCpu()
        mBuilder.setRomSize()
    }
}


fun main() {

    val pcBuilder = Pc.SurfaceBookBuilder()
    val surfaceBook = PcDirector(pcBuilder).getPc()
}

我们将 Pc 类中所有的 setter 都设置了 private,那么除了本类能够访问,其内部类也可以访问。
注意,此处建议使用静态内部类,Kotlin 中不作任何类的修饰词,默认相当于 Java 的静态内部内,使用 Java 记住给类添加 static 关键词。

其后我们获得的 surfaceBook 对象,就无法使用 setter 方法了。

5、常见变种

我们发现,两个 Builder 子类,其实现的方法都是可以动态实现的,且方法是被 Director 调用的,那么如果我们在 Director 动态实现的话,就完全可以省略多个 Builder子类,但是随之而来的问题就是,Dirctor 会变得臃肿,那么解决该问题的方法就是,直接省略 Dirctor 角色。

且在实现的过程中,如果我们在 Builder 类的每一个 setter 方法中,返回 this ,那么就可以使用链式的效果。最后的代码变成:

class Pc {

    private var mName: String? = null
    private var mCpu: String? = null
    private var mRomSize: Int? = null

    private fun setName(name: String) {
        this.mName = name
    }

    private fun setCpu(cpu: String) {
        this.mCpu = cpu
    }

    private fun setRomSize(romSize: Int) {
        this.mRomSize = romSize
    }

    fun printInfo() {
        print("笔记本的名字:$mName ,Cpu型号为:$mCpu")
    }

    class Builder {

        private var pc: Pc

        init {
            pc = Pc()
        }

        fun setName(name: String): Builder {
            pc.setName(name)
            return this
        }

        fun setCpu(cpu: String): Builder {
            pc.setCpu(cpu)
            return this
        }

        fun setRomSize(size: Int): Builder {
            pc.setRomSize(512)
            return this
        }

        fun createPc(): Pc {
            return pc
        }
    }

}


fun main() {
    
    val surfaceBook = Pc.Builder()
        .setName("SurfaceBook")
        .setCpu("SurfaceBook")
        .setRomSize(512)
        .createPc()

    surfaceBook.printInto()
}

三、相关源码

OkHttp3 的源码为例,我们先看看创建 OkHttpClient 实例的过程:

val okHttpClient = OkHttpClient.Builder()
            .callTimeout(1000,TimeUnit.SECONDS)
            .writeTimeout(1000,TimeUnit.SECONDS)
            ... ... //添加其他设置
            .build()

先看看整体的结构:

open class OkHttpClient internal constructor(builder: Builder) {

	... ...
	
	class Builder constructor() {
		... ...
	}
}

再看看 Builder 类中某个具体的方法,以 callTimeout() 为例,再看看最后的build()方法:

class Builder constructor() {
	... ...
	internal var callTimeout = 0
	
	... ...
	fun callTimeout(timeout: Long, unit: TimeUnit) = apply {
    	callTimeout = checkDuration("timeout", timeout, unit)
  	}

	fun build(): OkHttpClient = OkHttpClient(this)
}

我们看到,build() 方法最后是直接返回一个 new 的 OkHttpClient,并且将自身作为其构造函数的参数,那么我们返回 OkHttpClient 类中,看看其实现:

open class OkHttpClient internal constructor(builder: Builder){
	... ...
	val callTimeoutMillis: Int = builder.callTimeout
	... ...
}

最后,是将 Builder 中设置的属性,再次赋值到了 OkHttpClient 中。

不仅如此,OkHttpClient 还实现了 new Builder()的方法,用于在已有实例的基础上,重新设置其属性,具体的源码自行观阅去吧~
收工

四、小结

我们一定要明白建造者模式的精髓,那就是构建与表示分离!只要你基于这个原则,你想怎么玩就怎么玩~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值