让设计模式飞一下子|建造者模式

开头平常吹牛

你们好,我是高冷就是范儿,很久不见。最近比较忙,文章更新较慢,抱歉。😊今天咱们继续来聊设计模式这个话题。前面已经讲过几个模式,若是没有阅读过的朋友能够回顾一下。java

前文回顾
👉让设计模式飞一下子|②单例模式
👉让设计模式飞一下子|③工厂模式
👉让设计模式飞一下子|④原型模式算法

那么,今天咱们要来聊的是建造者模式,这也是GOF23的建立型模式中的最后一个模式。既然是建立型模式,天然又是一个用来建立对象的模式,这从这个模式的名字上也能看出来。数据库

dp-4-0

场景需求

建造者,就是用来建立对象的。这听上去就是一句废话......编程

对象不就是建造出来的吗?这怎么还成了一个单独的模式了?并且和前面说的那些模式有啥区别?设计模式

为了方便理解,我仍是不套用书上或者网上的一些理论,但就本身学过的一些理解,结合生活的一些场景跟你们说说。api

举个生活当中组装电脑的例子,好比如今小明去电脑城组装一台电脑,一台完整电脑确定会包含显示器,键盘,鼠标等等一些配件,而每一个配件的配置参数那就成百上千了,并且每一个硬件上都会有一些专业的参数咱们也看不懂。然而,咱们须要买的只是一台电脑,咱们只须要告诉卖电脑的人,咱们须要买什么品牌,什么档次,什么价格的电脑或配件便可,这样电脑商家就会给咱们组装好一台各方面都比较合适的电脑。小明平时就简单写个文档看个视频啥的,对配置要求不高,因此只须要入门级的电脑便可。以下图:数组

dp-4-1

而后小明玩了几年后上了大学,无心中染上了玩游戏的毛病,一发不可收拾,原来的电脑配置已经卡出翔了。因而,他又想换台游戏本,以下图:mybatis

dp-4-2

而后小明大学毕业了,由于大学每天玩游戏,毕业后实在混不下去了,据说学Java技术工资高,还能装逼,因而二话没说转行学Java......可是学Java再用原来那个破电脑可不行了,怎么样也得用个MacBook Pro才配得上程序猿这个高大上的职位嘛,因而......app

dp-4-3

上面这个是平常生活中很是常见的场景,那用代码如何去实现呢?框架

若是你以前没学过任何设计模式,你也许会以下设计(伪代码):

public class Computer {
    内存 
    CPU
    硬盘
    鼠标
		...
}

public class Client{
  void buy(){
    //入门级
    Computer comp = new Computer();
    comp.set内存("4g");
    comp.setCPU("i3");
    comp.set硬盘("128g");
    comp.set鼠标("杂牌");
    ...
  }
  把电脑交付给客户...
}
复制代码

这样写粗看没有什么毛病,咱们平时大部分时候就是这样写的。

那想一想,这样写后续会有什么问题吗?

如今小明电脑要升级了,换成游戏本,这个时候麻烦来了,他得去修改上面buy()方法体内的代码了,将原来的

comp.set内存("4g");
comp.setCPU("i3");
comp.set硬盘("128g");
comp.set鼠标("杂牌");
复制代码

修改为新的配置:

comp.set内存("8g");
comp.setCPU("i5");
comp.set硬盘("256g");
comp.set鼠标("罗技牌");
复制代码

再升级成MacBook Pro时也同理。并且若是我须要增长或者减小一些配置也一样须要去修改原来的代码。

显然这个是违反“开闭原则”了。

那问题出在哪里呢?

不难看出,compsetXX()系列方法和Client的api牢牢耦合了,并且Client须要对象建立的细节一清二楚,不然无法完成对象建立。

那怎么样才能把对象的建立过程单独从客户端代码中剥离出来,实现解耦呢?

若是还记得前面学过的工厂模式,有的同窗可能就会想到,工厂模式不就是解决这个问题的吗?把对象建立和对象的使用分离实现解耦,使得客户端并不须要关注对象的建立过程,须要与对应的工厂类打交道便可。由于咱们这边会涉及到多个对象,并且这多个对象之间也存在一些关联,因此此处能够采用抽象工厂模式实现。因此通过优化,你可能会以下实现:

interface ComputerFactory{
  内存 create内存();
  CPU createCPU();
  ...
}
复制代码

而后针对不一样定位的电脑,建立其不一样的Factory实现类便可。

class 入门级 implements ComputerFactory{
	内存 create内存(){
    生产4G内存
  }
  CPU createCPU(){
    生产i3内存
  }
  ...
}
复制代码

而后Client的代码会修改成以下:

//入门级
public class Client{
  ComputerFactory factory;
  void buy(){    
    Computer comp = new Computer();
    comp.set内存(factory.create内存());
    comp.setCPU(factory.createCPU());
    ...
  }
  把电脑交付给客户...
}
复制代码

这样的话,若是我须要升级配置,只须要传入不一样的ComputerFactory就能够了,业务逻辑的代码是不须要再动了。可是对于这个需求,这样的解决方法是存在一些问题的。为何呢?

以前在讲工厂模式的时候说过,工厂模式主要是用来生产一个完整的产品的。也就是说,用工厂模式建立出来的对象,就是一个最终的产品了。虽然抽象工厂模式能够一会儿建立多个产品,可是这多个产品其自己就是一个完整的最终的产品,直接就可使用了,无非是抽象工厂模式建立的这些个产品之类有一些关联,属于同一个类型的东西。

可是咱们的这个需求中,像内存、CPU、鼠标等这些,对于电脑而言,都只是整台电脑的一个组成部分而已,他们并非一个完整的产品,须要将他们组合装配起来才能构成一个完整的电脑对象。由于这个需求中,咱们须要获得的是一个电脑对象,而并非内存、CPU、鼠标等这些个零件。

固然你也可使用工厂模式来建立这些个零件,可是,以后你也还须要本身去对这些零部件进行组装,也就是说,你仍是须要对这个对象的组成细节了解清楚,不然,你仍是没法建立出一个完整的对象。另外,还会出现多一个零件,少一个零件的问题,会增长客户端的复杂度。

因而,咱们今天要讲的主角——建造者模式就闪亮登场了。

解决需求

建造者模式要解决的场景就是这个需求,致力于将一系列琐碎的零部件组装成一个完整的对象,而这其中具体的组装细节客户端是不须要知道的。

在建造者模式中,有一个抽象接口,里面会定义一系列对象零部件的装配的方法(组装内存、CPU、鼠标等这些个零件)。而后会有一个组装的人(好比电脑卖家),对这些个零部件进行组装,最后给客户端返回一个完整的对象。就好比上面组装电脑的例子。

dp-4-4

实现代码以下:

//抽象接口定义完整对象的零部件装配方法
abstract class ComputerBuilder{
    protected Computer comp = new Computer(); 
    abstract void build内存();
    abstract void buildCPU();
    abstract void build硬盘();
  	protected Computer getComputer(){
        return comp;
    }
}
复制代码

而后针对于不一样级别的电脑,能够建立其对应的抽象接口的实现类,以完成对电脑的装配,好比如今须要装配一台MacBook Pro,则实现以下:

class MacBookProBuilder extends ComputerBuilder{
    void build内存() {
        comp.set内存("16g");
    }
    void buildCPU() {
        comp.setCPU("i7");
    }
    void build硬盘() {
        comp.set硬盘("1T");
    }
}
复制代码

还要须要一个专门负责装配的对象,好比电脑卖家,

class Seller{
  ComputerBuilder builder;
  Computer sell(){
    builder.build内存();
    builder.buildCPU();
    builder.build硬盘();
    return builder.getComputer();
  }
}
复制代码

这个时候在Client中的代码变成了以下:

//MacBook Pro
public class Client{
  Seller seller;
  void buy(){    
    Computer comp = seller.sell();
  }
  把电脑交付给客户...
}
复制代码

咱们会发现,此时,客户端已经完成解耦,咱们只须要告诉电脑卖家咱们须要什么级别的电脑,他就会给咱们返回一台装配好的完整的对象,不再须要去了解各个零件是怎么生产的(是经过单例模式建立的?仍是原型模式?仍是工厂模式?),也不须要知道这些零部件是怎么拼装起来的(是先装配CPU,仍是先装配硬盘?)。这样就完美的解决了解耦的问题。

如今假设小明想再换一个配置的电脑,只须要再提供一个对应的Buidler子类,完成对应零件的建立,而后将Builder交给负责装配的人(术语叫Director),如卖家,就能够了,Client不再须要有任何的改动,符合“开闭原则”。

在对象没有那么复杂的状况下,Director也是能够省略的,直接将装配过程在Client端实现便可。固然在这种状况下,Client是须要对该对象的组成结构有所了解的,也容易致使缺胳膊少腿的状况。

另外,上面的代码其实能够修改成更为优雅的写法。

class MacBookProBuilder extends ComputerBuilder{
    void build内存() {
        comp.set内存("16g");
      	return this;
    }
    void buildCPU() {
        comp.setCPU("i7");
      	return this;
    }
    void build硬盘() {
        comp.set硬盘("1T");
      	return this;
    }
}
复制代码

在每个装配的方法中都返回当前Builder对象,这样Director中装配逻辑能够直接以清爽简洁的链式风格书写。

class Seller{
  ComputerBuilder builder;
  Computer sell(){
    return builder.build内存().buildCPU().build硬盘().getComputer();
  }
}
复制代码

这种链式风格能够很好的用来解决伸缩构造器反模式的问题。什么意思?

好比如今某一个类,属性极多,有上百个吧......

class A{
  属性1;
  属性2;
  属性3;
  属性4;
  ...省略100个属性
}
复制代码

如今我须要在某处建立该类对象,而且还须要对其中某一些属性进行赋值。若是这个时候咱们采用构造器来建立就会比较麻烦,为何呢?

由于我可能每一次建立所须要的属性多是不同的。好比在应用某处须要建立一个A对象,须要使用属性一、属性二、属性3,因而我会在A类中加入一个构造器,

class A {
  //省略属性
  public A(属性1,属性2,属性3){}
}
复制代码

在应用另一处又须要建立一个A对象,须要使用属性一、属性二、属性三、属性4,因而你又须要添加一个构造器,

class A {
  //省略属性
  public A(属性1,属性2,属性3,属性4){}
}
复制代码

能够想象,要是各处引用构造器特别多,而且参数还都不同,那画面太美不敢想象。还有,当构造器重载太多,建立对象时选择合适构造器都是一件很费神的事情。这个时候使用建造者模式的链式风格就很好的解决了这个问题。

class ABuilder{
  A a = new A();
  A set属性1(xxx){... return this;}
  A set属性2(xxx){... return this;}
  A set属性3(xxx){... return this;}
  A set属性4(xxx){... return this;}
  A build(){return a;}
 ...
}
复制代码

这个时候我再建立一个A对象,以下,若是我须要修改属性设置的个数,能够很方便的进行调整,很好的解决了重载构造器的问题。

A a = new ABuilder().set属性1(xxx),set属性2(xxx).set属性3(xxx).set属性4(xxx).build();
复制代码

总结

关于建造者模式的核心内容就这些,咱们能够作一下总结。

  • 做为建立型模式,建造者模式也是用来建立对象的,并且他和工厂模式看上去会比较类似,甚至难以区分。

    • 不过建造者模式关键点在于建造和装配分离。建造者最终只会生成一个完整的对象,可是这个对象通常来讲是比较复杂的,里面会分红好几个模块,建造者模式强调的是这个装配的过程。

    • 而工厂模式,主要强调的是建立,固然这个建立有多是会同时建立一个(简单工厂或者工厂方法模式)或者多个对象(抽象工厂模式)。虽然工厂模式建立的也有可能很复杂,可是他不关心对象会不会有装配的过程,只要建立出来便可。

      以下图:

      dp-4-5

      一言以蔽之,工厂模式强调建立,建造者模式强调组装。

      其实设计模式这东西并非很绝对很孤立的去看待,所以咱们没有必要将每个模式的区别都分得特别明确,通常来讲,设计模式也不是独立使用的,会相互搭配。就好比这边得工厂模式和建造者模式,二者侧重点不一样,可是彻底能够结合使用。好比在使用工厂模式建立对象时,有可能每一个对象都会有比较明确的装配过程,就能够结合使用。反过来吗,在使用建造者模式时,每一步的装配所须要的零件,又有多是经过工厂模式(固然也有多是经过原型模式,单例模式)建立所得。

  • 在建造者模式中,客户端只须要与Director交互,并不须要知道内部的对象构建和装配的细节,屏蔽了系统复杂度。

  • 能够为系统中添加多个Builder,好比上面例子中为不一样品牌的电脑分别建立一个Builder对象,来达到扩展系统功能的目的。同时,经过调整每个Builder内部装配的过程,有可能轻松对装配过程当中的每一步进行细粒度的控制和定制。

存在的问题

没有一个设计模式是完美的,每个设计模式都有其特定的使用场景。经过上面分析,不难发现,

  • 建造者模式主要用于建立那些具备明显组装过程的一类复杂对象,而且这类对象中内部结构差别性都不大,基本结构都相同,而且比较稳定。好比上面的电脑的例子,无论啥牌子的电脑,啥级别的电脑,配件都是那些,CPU,内存,硬盘等,变不出花来了,无非就是每种配件的具体参数不一样。能够想象,若是是两类差别性很大的对象,一类是电脑,一类是汽车,彻底是八竿子打不着的两类产品,装配过程更是天差地别,天然是无法使用建造者模式的。

    一个类的各个组成部分的具体实现类或者算法常常面临着变化,可是将他们组合在一块儿的算法却相对稳定。建造者模式提供一种封装机制,将稳定的组合算法于易变的各个组成部分隔离开来。

  • 建造者模式建立的对象通常内部变化是不大,不频繁的。对于变更很频繁的也是不适合用建造者模式的。就像买电脑,你三天两头换电脑,升级配置,而每升级一套配置,就须要从新建立一个全新的Builder类,若是变更太多,系统中就须要维护大量的Builder类,增长系统复杂度和维护难度。

抛砖引玉

最后我举两个我知道的在实际框架或者JDK源码中使用建造者模式的例子。其实建造者模式在实际开发中应用也是很是普遍的,并且也比较好识别,基本以xxxBuilder命名的都是使用了建造者模式。

好比JDK中,StringBuilder类是一个经典的建造者模式实现,

//经典用法
String s = new StringBuilder("a").append("b").append("c").toString();
复制代码

StringBuilder继承自AbstractStringBuilder,这是一个抽象类,在它里面定义了一个属性value数组。

abstract class AbstractStringBuilder implements Appendable, CharSequence {
	//省略无关代码
    char[] value;
  //省略无关代码
}
复制代码

当咱们调用StringBuilder的构造器或者是append()方法时,其实是对该value数组操做。好比append()为例,

public StringBuilder append(CharSequence s) {
   super.append(s);
   return this;
}
复制代码

他会去调用父类的append()方法,同时会return this,实现链式风格的编程。在父类append()方法中,最终会调用以下:

public AbstractStringBuilder append(CharSequence s, int start, int end) {   
  //省略一些校验扩容操做
  for (int i = start, j = count; i < end; i++, j++)
    value[j] = s.charAt(i);
  count += len;
  return this;
}
复制代码

其实就是往value数组中设置传入的字符串值。有点像ArrayList的感受。

当调用toString()方法时,其实就是把value数组的内容转成String返回罢了。

public String toString() {
  return new String(value, 0, count);
}
复制代码

另外,在Mybatis中也是大量使用了建造者模式。在Mybatis启动时,会作一系列的解析工做,好比mybatis-config.xml文件解析,各Mapper.xml,还有Mapper接口上的注解等,这一系列的解析工做都是经过一系列的Builder完成的。顶层是BaseBuilder类。这些Builder的层次结构以下图,好比,XMLConfigBuilder就是用来解析mybatis-config.xml文件,XMLMapperBuilder用来解析各Mapper.xml等。

dp-4-6

在这里,BaseBuilder做为一个顶层抽象接口,里面只定义了一些全部具体Builder的属性和方法,好比全局配置Configuration对象。而其他Builder子类做为具体建造者完成各自的解析工做。咱们这边以XMLConfigBuilder为例,XMLConfigBuilder.parse()方法是解析的核心方法。里面会调用BseBuilder中的parseConfiguration()方法。parseConfiguration()方法就是整个装配流程,

private void parseConfiguration(XNode root) {
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));
}
复制代码

咱们以propertiesElement为例,这个方法会使用XPATH解析properties字段中的resourceurl属性,而且会将其设置到定义在BaseBuilder中的configuration中。其他的解析方法同理,就再也不赘述了。当全部的装配工做完成以后,XMLConfigBuilder.parse()就会将解析后的Configuration对象返回。在SqlSessionFactoryBuilderbuild()方法中会调用XMLConfigBuilder.parse(),根据组装好的Configuration对象生成SqlSessionFactory,进而建立SqlSession以操做数据库。

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      return build(parser.parse());
    //省略异常处理
  }
复制代码

好了,今天关于建造者模式的技术分享就到此结束。随着本次建造者模式的结束,GOF23的建立型模式就都讲完了。从下一篇开始,咱们会进入另一大类——结构型模式的世界。结构型模式的模式主要关注点是如何经过必定手段,将两个或多个对象组合造成一个更大更强大的对象。下一篇我就将从结构型模式中最重要,也是最难理解的一个设计模式——代理模式开始讲起,具体咱们下期再说,敬请期待。😊👏

相关文章
相关标签/搜索