Java中的设计模式(六):原型模式

106 阅读5分钟

image.png

一、基本概念

原型模式主要面对的问题是:“某些结构复杂的对象”的创建工作;由于需求的变化,这些对象经常面临着剧烈的变化,但是他们却拥有比较稳定一致的接口。

原型模式 (Prototype Pattern)是 创建型设计模式 的一种,原型模式允许一个对象再创建另外一个可定制的对象,根本无需知道任何如何创建的细节,其工作原理是:通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝它们自己来实施创建。

二、模式原理分析

image.png

使用者 需要建立一个原型,才能基于原型拷贝出新实例,还需要决策什么时候使用原型、什么时候使用新实例以及从原型到新实例之间的拷贝应该采用什么样的算法策略

查看如下代码:

public interface PrototypeInterface extends Cloneable {
    PrototypeInterface clone() throws CloneNotSupportedException;
}
public class ProtypeA implements PrototypeInterface {
    @Override
    public ProtypeA clone() throws CloneNotSupportedException {
        System.out.println("Cloning new object: A");
        return (ProtypeA) super.clone();
    }
}
public class ProtypeB implements PrototypeInterface {
    @Override
    public ProtypeB clone() throws CloneNotSupportedException {
        System.out.println("Cloning new object: B");
        return (ProtypeB) super.clone();
    }
}

//ProtypeA以自己为原型通过拷贝创建一个新的对象newInstanceA

public static void main(String[] args) throws CloneNotSupportedException {
    ProtypeA source = new ProtypeA();
    System.out.println(source);
    ProtypeA newInstanceA = source.clone();
    System.out.println(newInstanceA);
}

原型模式封装了如下变化:

  • 原始对象的构造方式,clone 方法的原理是从内存种以二进制流的方式进行拷贝,重新分配内存块,所以构造函数不会执行
  • 对象的属性与其他对象间的依赖关系
  • 对象运行时状态的获取方式
  • 对象拷贝的具体实现策略 clone 方法的实现

所以说,原型模式从建立原型到拷贝原型生成新实例,都是对用户透明的,一旦中间任何一个小细节出现问题,你可能获取的就是一个错误的对象

三、模式的实施

原型模式的实施又分为浅拷贝深拷贝,拷贝就是进行简单的值拷贝,所谓值拷贝就是值拷贝基本的八大数据类型,引用类型不拷贝仍然使用原对象的引用类型对象的地址;深拷贝就是无论是简单的值还是引用数据类型全部都进行拷贝一份!

3.1 浅拷贝

public class Test implements Cloneable{
    private ArrayList<String> arrayList = new ArrayList<>();
    @Override
    public Test clone(){
        Test test = null;
        try {
            test = (Test)super.clone();
        } catch(CloneNotSupportedException e){
            
        }
        return test;
    }
    public void setValue(String value){
        this.arrayList.add(value);
    }
    public ArrayList<String> getValue(){
        return this.arrayList;
    }
}
public class TestDemo{
    public static void main(String[] args){
        Test test = new Test();
        test.setValue("我");
        
        Test cloneTest = test.clone();
        cloneTest.setValue("你");
        System.out.println(test.getValue());
    }
}
输出:

[我,你]

按我们的意愿,应该只输出 “我”,克隆对象不应该改变原型对象的值,这里却改变了,为什么呢?

Object 类提供的方法clone只是拷贝对象,其内部的数组、引用对象等都不拷贝,还是指向原型对象的内部元素地址,这种拷贝就叫浅拷贝。非常不安全。但是内部的基本类型,例如int、long、char 都会被拷贝,对于String类型,可以理解为是基本类型,它没有clone方法。

3.2 深拷贝

我们对上面的clone代码修改一下

    @Override
    public Test clone(){
        Test test = null;
        try{
            test = (Test)super.clone();
            test.arrayList = (ArrayList<String>)this.arrayList.clone();
        }catch(CloneNotSupportedException e){
            
        }
        return test;
    }

其实就是单独对私有变量拷贝就可以了,这种方式的拷贝,两个对象间就没有任何瓜葛了,这就是深拷贝。

当然,深拷贝还可以通过自己写二进制流的方式来操作,类似下面的操作:

  public Test deepClone() throws IOException,ClassNotFoundException{
        //将对象放入流
        ByteArrayOutputStream bao = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bao);
        oos.writeObject(this);
        //将象从流中取出
        ByteArrayInputStream bis = new ByteArrayInputStream(bao.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        return (Test)ois.readObject();
    }

四、使用场景

  • 资源优化场景。也就是当进行对象初始化需要使用很多外部资源时,比如,IO 资源、数据文件、CPU、网络和内存等。
  • 复杂的依赖场景。 比如,F 对象的创建依赖 A,A 又依赖 B,B 又依赖 C……于是创建过程是一连串对象的 get 和 set。
  • 性能和安全要求的场景。 比如,同一个用户在一个会话周期里,可能会反复登录平台或使用某些受限的功能,每一次访问请求都会访问授权服务器进行授权,但如果每次都通过 new 产生一个对象会非常烦琐,这时则可以使用原型模式。
  • 同一个对象可能被多个修改者使用的场景。 比如,一个商品对象需要提供给物流、会员、订单等多个服务访问,而且各个调用者可能都需要修改其值时,就可以考虑使用原型模式。
  • 需要保存原始对象状态的场景。 比如,记录历史操作的场景中,就可以通过原型模式快速保存记录。
  • 结合工厂模式来使用。 在实际项目中,原型模式除了单独基于对象使用外,还可以结合工厂方法模式一起使用,通过定义统一的复制接口,比如 clone、copy。使用一个工厂来统一进行拷贝和新对象创建, 然后由工厂方法提供给调用者使用。

五、原型模式的优点和缺点

5.1 原型模式的优点

  • 逃避了构造函数的约束
  • 减少每次创建对象的资源消耗
  • 降低对象创建的时间消耗
  • 快速复制对象运行时状态,复制大对象时,性能更优
  • 能保存原始对象的副本

5.2 原型模式的缺点

  • 原型需要一个被初始化过的正确对象
  • 复制大对象时,可能出现内存溢出的 OOM 错误
  • 动态扩展对象功能时可能会掩盖新的风险。比如,埋点服务中我们通常会拷贝一份对象在某个时间节点的数据,并添加一些追踪数据后再推送给埋点服务,这样就可能增加过多的内存消耗,影响原有功能执行的性能,有时还可能引起 OOM,导致系统宕机

本文绝大部分内容转载自:juejin.cn/post/697360…