Design Patterns Of Prototype And Builder


一、原型模式

在有些系统中,存在大量相同或相似对象的创建问题,如果用传统的构造函数来创建对象,会比较复杂且耗时耗资源,用原型模式生成对象就很高效,就像孙悟空拔下猴毛轻轻一吹就变出很多孙悟空一样简单。

1、传统方法

Sheep sheep = new Sheep("tom", 1, "白色");

// 传统方法复制
Sheep sheep2 = new Sheep(sheep.getName(), sheep.getAge(), sheep.getColor());

优缺点:

  • 好理解,易操作
  • 创建新的对象总是需要重新获取原始对象的属性,如果对象比较复杂,效率就会变低
  • 总是需要重新初始化对象,而不是动态的获得对象,不够灵活
  • 改进:
    • 思路:Java Object 类提供了一个 clone() 方法,该方法可以将 Java 对象复制一份,但是需要实现 clone 的 Java 类必须实现 Cloneable 接口(原型模式

2、原型模式(优缺点)

基本介绍:

  1. 原型模式(Prototype)是指:用原型实例创建对象的种类,并且通过拷贝这些原型,创建新的对象
  2. 原型模式是一种创建型设计模式,允许一个对象再创建另外一个可定制的对象,无需知道如如何创建的细节
  3. 工作原理:通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝它们自己来实现创建,即:对象.clone()

结构:

实现:

  • Java 中的 Object 类提供了浅克隆的 clone() 方法,具体原型类只要实现 Cloneable 接口就可实现对象的浅克隆,这里的 Cloneable 接口就是抽象原型类。

说明:

  1. 抽象原型类:规定了具体原型对象必须实现的接口
  2. 具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。
  3. 访问类:使用具体原型类中的 clone() 方法来复制新的对象。

优缺点:

  • 优点:
    • Java 自带的原型模式基于内存二进制流的复制,在性能上比直接 new 一个对象更加优良。
    • 可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份,并将其状态保存起来,简化了创建对象的过程,以便在需要的时候使用(例如恢复到历史某一状态),可辅助实现撤销操作。
  • 缺点:
    • 需要为每一个类都配置一个 clone 方法
    • clone 方法位于类的内部,当对已有类进行改造的时候,需要修改代码,违背了开闭原则。
    • 当实现深克隆时,需要编写较为复杂的代码,而且当对象之间存在多重嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来会比较麻烦。因此,深克隆、浅克隆需要运用得当。

3、Spring 源码应用

Spring 中原型 bean 的创建,就使用了原型模式

<bean id="people" class="com.naivekyo.springanalysis.People" scope="prototype">
    <property name="name" value="张三"/>
    <property name="age" value="21"/>
</bean>

4、浅拷贝和深拷贝

浅拷贝

  1. 对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象
  2. 对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因此实际上两个对象的该成员变量都指向同一个实例,这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量
  3. Java 中的 Object 类提供了浅克隆的 clone() 方法,具体原型类只要实现 Cloneable 接口就可实现对象的浅克隆,这里的 Cloneable 接口就是抽象原型类。

深拷贝

  1. 复制对象的所有基本数据类型的成员变量值
  2. 为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象进行拷贝。
  3. 深拷贝实现方式:
    • 1:重写 clone 方法实现深拷贝
    • 2:通过对象序列化实现深拷贝

5、深拷贝详解

方式一:重写 clone 方法

// 深拷贝:clone()

@Override
protected Object clone() throws CloneNotSupportedException {

    Object deep = null;

    // 完成对基本数据类型的克隆
    deep = super.clone();
    // 单独处理引用类型
    DeepPrototype deepPrototype = (DeepPrototype) deep;
    deepPrototype.deepCloneableTarget = (DeepCloneableTarget) this.deepCloneableTarget.clone();

    return deepPrototype;
}

方式二:序列化实现深拷贝(推荐使用)

// 深拷贝:序列化(推荐)

public Object deepClone() {
    // 创建流对象
    ByteArrayOutputStream bos =  null;
    ByteArrayInputStream bis = null;

    ObjectOutputStream oos = null;
    ObjectInputStream ois = null;

    try {
        // 序列化操作
        bos = new ByteArrayOutputStream();
        oos = new ObjectOutputStream(bos);
        oos.writeObject(this);  // 把当前对象转换成字节流

        // 反序列化
        bis = new ByteArrayInputStream(bos.toByteArray());
        ois = new ObjectInputStream(bis);
        DeepPrototype copyObj = (DeepPrototype) ois.readObject();

        return copyObj;
    } catch (IOException | ClassNotFoundException e) {
        e.printStackTrace();
        return null;
    } finally {
        // 关闭流
        try {
            bos.close();
            oos.close();
            bis.close();
            ois.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

6、原型模式注意事项

  1. 创建新的对象比较复杂时,可以利用原型模式简化对象的创建过程,同时也能提高效率
  2. 不用重新初始化对象,而是动态的获得对象运行时的状态
  3. 如果原始对象发生变化(增加或者减少属性),其他克隆对象也会发生相应的变化,无需修改代码
  4. 再实现深克隆的时候可能需要比较复杂的代码(重写 clone 方法)
  5. 缺点:需要为每一个类配备一个克隆方法,这对全新的类来说不是很复杂,但对已有的类进行改造时,需要修改源代码,违背了 ocp 原则。

7、原型模式的应用场景

原型模式通常适用于以下场景。

  • 对象之间相同或相似,即只是个别的几个属性不同的时候。
  • 创建对象成本较大,例如初始化时间长,占用CPU太多,或者占用网络资源太多等,需要优化资源。
  • 创建一个对象需要繁琐的数据准备或访问权限等,需要提高性能或者提高安全性。
  • 系统中大量使用该类对象,且各个调用者都需要给它的属性重新赋值。

二、建造者模式

建造者模式将复杂对象中变化与不变的部分分离开

举个例子:盖房子问题

盖房子的步骤是确定的,打地基、砌墙、封底

但是建成房子的种类有很多:普通房子、高楼 等等

1、传统方式

AbstractHouse

— CommonHouse

Client

  1. 优点:实现比较简单
  2. 设计的程序结构:没有设计缓存层对象,程序的扩展和维护不好,也就是说,这种设计方式:将产品和创建产品的过程封装在一起,增强了耦合性
  3. 解决方案:将产品和产品建造过程解耦 =》 建造者模式

2、建造者模式

基本介绍

  • 建造者模式(Builder Pattern)又叫生成器模式,是一种对象构建模式,它可以将复杂对象的建造过程抽象出来(抽象类别),使这个抽象过程的不同实现方式可以构造出不同表现(属性)的对象
  • 建造者模式 是一步一步创建一个复杂的对象,它允许用户只通过指定复杂对象的类型和内容就可以构建它们,用户不需要知道内部的具体实现细节

模式的结构和实现

建造者(Builder)模式由产品、抽象建造者、具体建造者、指挥者等 4 个要素构成,现在我们来分析其基本结构和实现方法。

模式的结构:

建造者(Builder)模式的主要角色如下。

  1. 产品角色(Product):它是包含多个组成部件的复杂对象,由具体建造者来创建其各个零部件。
  2. 抽象建造者(Builder):它是一个包含创建产品各个子部件的抽象方法的接口 / 抽象类,通常还包含一个返回复杂产品的方法 getResult()。
  3. 具体建造者(Concrete Builder):实现 Builder 接口,完成复杂产品的各个部件的具体创建方法。
  4. 指挥者(Director):它调用建造者对象中的部件构造与装配方法完成复杂对象的创建,在指挥者中不涉及具体产品的信息。

类图:

图中抽象建造者和指挥者之间应该是 组合 关系。箭头用错了。

Java 的 StringBuilder 使用了建造者模式的思想

注意事项

  • 客户端(实用程序)不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品
  • 每一个具体的建造者都相对独立,而与其他的具体建造者无关,因此可以很方便的替换具体建造者或增加新的具体建造者,用户使用不同的具体建造者即可得到不同的产品对象
  • 可以更加精细地控制产品的创建过程。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。
  • 增加新的具体建造者无需修改原有的代码,指挥者类针对抽象建造者编程,系统扩展方便,符合 ocp 原则。
  • 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围收到一定的限制
  • 如果产品内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,如果系统变得很庞大,因此在这种情况下,要考虑是否使用建造者模式
  • 抽象工厂模式 VS 建造者模式
    • 抽象工厂模式实现对产品家族的创建,一个产品家族是这样的一系列产品:具有不同分类维度的产品组合,采用抽象工厂模式不需要关心创建过程,只关心什么产品由什么工程生产即可
    • 而建造者模式则是要求按照指定的蓝图建造产品,它的主要目的是通过组装零配件来生产另一个新产品。

3、模式的应用场景

建造者模式唯一区别于工厂模式的是针对复杂对象的创建。也就是说,如果创建简单对象,通常都是使用工厂模式进行创建,而如果创建复杂对象,就可以考虑使用建造者模式。

当需要创建的产品具备复杂创建过程时,可以抽取出共性创建过程,然后交由具体实现类自定义创建流程,使得同样的创建行为可以生产出不同的产品,分离了创建与表示,使创建产品的灵活性大大增加。

建造者模式主要适用于以下应用场景:

  • 相同的方法,不同的执行顺序,产生不同的结果。
  • 多个部件或零件,都可以装配到一个对象中,但是产生的结果又不相同。
  • 产品类非常复杂,或者产品类中不同的调用顺序产生不同的作用。
  • 初始化一个对象特别复杂,参数多,而且很多参数都具有默认值。

4、建造者模式和工厂模式的区别

  • 建造者模式更加注重方法的调用顺序,工厂模式注重创建对象。
  • 创建对象的力度不同,建造者模式创建复杂的对象,由各种复杂的部件组成,工厂模式创建出来的对象都一样
  • 关注重点不一样,工厂模式只需要把对象创建出来就可以了,而建造者模式不仅要创建出对象,还要知道对象由哪些部件组成。
  • 建造者模式根据建造过程中的顺序不一样,最终对象部件组成也不一样。

5、模式的扩展

建造者(Builder)模式在应用过程中可以根据需要改变,如果创建的产品种类只有一种,只需要一个具体建造者,这时可以省略掉抽象建造者,甚至可以省略掉指挥者角色。


Author: NaiveKyo
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source NaiveKyo !
  TOC