前言
原型模式是一种创建型设计模式, 使你能够复制已有对象, 而又无需使代码依赖它们所属的类。
原型模式
原型模式(Prototype Design Pattern)的定义用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象。
不知道各位看官看了《黑亚当》了吗,电影中的命运博士能根据自身的形态,复制多个"命运博士",这种技巧,就是我们所说的"原型模式",而命运博士本身就是原型对象。
原型模式结构
原型模式原理
它包含如下角色:
- 抽象原型类(Prototype):它是声明克隆方法的接口,是所有具体原型类的公共父类,它可以是抽象类也可以是接口类型。
- 具体原型类(ConcretePrototype):实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。
- 客户端类(Client):在客户端类中,让一个原型对象克隆自身从而创建一个新的对象。由于客户类针对抽象原型类Prototype编程。因此用户可以根据需要选择具体原型类,系统具有较好的扩展性,增加或者替换具体原型类都比较方便。
深克隆与浅克隆
根据在"复制原型对象的同时是否复制包含在原型对象中引用类型的成员变量 "这个条件,原型模式的克隆机制分为两种:深克隆(Deep Clone)、浅克隆(Shallow Clone)。
1、浅克隆
被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象(也就是克隆对象与原型对象共享引用数据类型变量)。
Java中的Object类中提供了clone方法来实现浅克隆,需要注意的是要想实现克隆的Java类必须实现一个标识接口Cloneable,来表示这个Java类支持被复制,如下:
/**
* Cloneable接口是上面的类图中的抽象原型类,而实现了Cloneable接口的子实现类就是具体的原型类。
* @author Duansg
* @date 2022-12-20 12:16 上午
*/
@Data
@Slf4j
public class Prototype implements Cloneable {
private Context context;
public Prototype() {
log.info("init constructor");
}
@Override
public Prototype clone() {
try {
return (Prototype) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
public static void main(String[] args) {
// step1
Prototype prototype = new Prototype();
prototype.setContext(new Context());
// step2
Prototype clonePrototype = prototype.clone();
// true
System.out.println(prototype.getContext() == clonePrototype.getContext());
}
}
2、深克隆
除去那些引用其他对象的变量,被复制对象的所有变量都含有与原来的对象相同的值。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。也就是说:深克隆把要复制的对象所引用的对象都复制了一遍。
如果有需求场景中不允许共享同一对象,那么就需要使用深克隆,如果想要进行深拷贝需要使用到对象序列化流 (对象序列化之后,再进行反序列化获取到的是不同对象),如下所示:
/**
* 必须实现Serializable接口,否则会抛NotSerializableException异常。
* @author Duansg
* @date 2022-12-20 12:16 上午
*/
@Data
@Slf4j
public class Prototype implements Cloneable, Serializable {
private static final long serialVersionUID = -5861995997720845177L;
private Context context;
public Prototype() {
log.info("init constructor");
}
@Override
public Prototype clone() {
try {
return (Prototype) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
Prototype prototype = new Prototype();
prototype.setContext(new Context());
// 序列化输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("prototype.obj"));
// 将对象写到文件中
oos.writeObject(prototype);
oos.close();
// 序列化输入流
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("prototype.obj"));
// 读取对象
Prototype clonePrototype = (Prototype)ois.readObject();
// false
System.out.println(prototype.getContext() == clonePrototype.getContext());
}
}
其实现在不推荐大家用Cloneable接口,实现比较麻烦,现在借助Apache-Commons或者Springframework可以直接实现:
- 浅克隆:
public static void main(String[] args) throws Exception {
Prototype prototype = new Prototype();
prototype.setContext(new Context());
Prototype clonePrototype = (Prototype) BeanUtils.cloneBean(prototype);
// true
System.out.println(prototype.getContext() == clonePrototype.getContext());
clonePrototype = new Prototype();
BeanUtils.copyProperties(prototype, clonePrototype);
// true
System.out.println(prototype.getContext() == clonePrototype.getContext());
}
- 深克隆:
public static void main(String[] args) throws Exception {
Prototype prototype = new Prototype();
prototype.setContext(new Context());
Prototype clonePrototype = (Prototype) SerializationUtils.clone(prototype);
// false
System.out.println(prototype.getContext() == clonePrototype.getContext());
}
BeanUtils是利用反射原理获得所有类可见的属性和方法,然后复制到target类。而SerializationUtils.clone()就是使用我们的前面讲的序列化实现深克隆,当然你要把要克隆的类实现Serialization接口。
原型模式使用场景
常见的使用场景如下:
- 资源优化场景。也就是当进行对象初始化需要使用很多外部资源时,比如,IO 资源、数据文件、CPU、网络和内存等。
- 复杂的依赖场景。 比如,F 对象的创建依赖 A,A 又依赖 B,B 又依赖 C……于是创建过程是一连串对象的 get 和 set。
- 性能和安全要求的场景。 比如,同一个用户在一个会话周期里,可能会反复登录平台或使用某些受限的功能,每一次访问请求都会访问授权服务器进行授权,但如果每次都通过 new 产生一个对象会非常烦琐,这时则可以使用原型模式。
- 同一个对象可能被多个修改者使用的场景。 比如,一个商品对象需要提供给物流、会员、订单等多个服务访问,而且各个调用者可能都需要修改其值时,就可以考虑使用原型模式。
- 需要保存原始对象状态的场景。 比如,记录历史操作的场景中,就可以通过原型模式快速保存记录。
原型模式主要解决的问题
如果创建对象的成本比较大,比如对象中的数据是经过复杂计算才能得到,或者需要从RPC接口或者数据库等比较慢的IO中获取,这种情况我们就可以使用原型模式,从其他已有的对象中进行拷贝,而不是每次都创建新对象,进行一些耗时的操作。
总结
优点
- 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个已有实例可以提高新实例的创建效率。
比如,在 AI 系统中,我们经常需要频繁使用大量不同分类的数据模型文件,在对这一类文件建立对象模型时,不仅会长时间占用 IO 读写资源,还会消耗大量 CPU 运算资源,如果频繁创建模型对象,就会很容易造成服务器 CPU 被打满而导致系统宕机。通过原型模式我们可以很容易地解决这个问题,当我们完成对象的第一次初始化后,新创建的对象便使用对象拷贝(在内存中进行二进制流的拷贝),虽然拷贝也会消耗一定资源,但是相比初始化的外部读写和运算来说,内存拷贝消耗会小很多,而且速度快很多
- 原型模式提供了简化的创建结构,工厂方法模式常常需要有一个与产品类等级结构相同的工厂等级结构(具体工厂对应具体产品),而原型模式就不需要这样,原型模式的产品复制是通过封装在原型类中的克隆方法实现的,无须专门的工厂类来创建产品。
- 可以使用深克隆的方式保存对象状态,使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用,比如恢复到某一历史状态,可以辅助实现撤销操作。
在某些需要保存历史状态的场景中,比如,聊天消息、上线发布流程、需要撤销操作的程序等,原型模式能快速地复制现有对象的状态并留存副本,方便快速地回滚到上一次保存或最初的状态,避免因网络延迟、误操作等原因而造成数据的不可恢复。
缺点
- 需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类进行改造时需要修改源代码,违背了开闭原则。