设计模式-原型模式
原型模式 prototypePattern
原型模式原理与介绍
原型模式其实就是用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象。
比如在《西游记》当中的猴哥用自己的猴毛克隆了多个与自己一模一样的本体,这就是对象原型,而猴哥(克隆本体)就是原型,变出千千万万个克隆的对象。
原型模式主要解决的问题
如果说一个对象内部是足够复杂,对象足够大,内部的数据是要通过大量的精密计算才能得到的,或者说需要通过 RPC 的接口或者数据库等比较慢的 IO 中获取,这种情况我们就需要用到原型模式。
直接从原有的现成对象模型当中克隆一份,就不需要每一次都需要创建一个新的对象,进行一些费事的操作,可以节省大量的时间与资源。
原型模式的角色
抽象原型类(Prototype):它是声明克隆方法的接口,是所有具体原型类的公共父类,它可以是抽象类也可以是接口
具体原型类(ConcretePrototype):实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象
客户类(Client):在客户类中,让一个原型对象克隆自身从而创建一个新的对象,由于客户类针对抽象原型类Prototype编程.因此用户可以根据需要选择具体原型类,系统具有较好的扩展性,增加或者替换具体原型类都比较方便 。
深克隆与浅克隆
-
浅克隆
-
深克隆
在 Java 当中 Object 类提供了 clone() 方法实现了 浅克隆 。
需要注意的一点就是,如果要使得一个克隆对象的 Java 类支持被克隆复制,那么改克隆的对象类就必须实现一个表示接口 Cloenable ,用来表示该 Java 类支持被复制。
Cloenable 接口就是上面原型模式角色当中的抽象接口原型 (Prototype) ,而其被克隆的对象类就是具体原型类 (ConcretePrototype) ,而用户端(Client)调用需要克隆的原型类中的方法 clone() 生成其原型对象。
/**
* @Author Peggy
* @Date 2023-05-19 10:54
* 克隆对象 A
**/
public class ConcretePrototypeA implements Cloneable {
public User user = new User();
public ConcretePrototypeA() {
System.out.println("具体原型对象创建完成");
user.setName("卡卡罗特");
user.setAge(18);
}
@Override
public ConcretePrototypeA clone() throws CloneNotSupportedException {
System.out.println("具体原型对象复制成功");
return (ConcretePrototypeA) super.clone();
}
}
@Test
public void testCloen1() throws CloneNotSupportedException {
ConcretePrototypeA c1 = new ConcretePrototypeA();
c1.setUser(new User("卡卡罗特", 18));
ConcretePrototypeA c2 = c1.clone();
c2.user.setName("贝吉塔");
c2.user.setAge(19);
System.out.println("对象 c1 和 c2 是用一个对象?" + (c1 == c2));
System.out.println("对象 c1 和 c2 内部的 user 是同一个对象?" + (c1.user == c2.user));
System.out.println("对象 c1.user "+c1.getUser());
System.out.println("对象 c2.user "+c2.getUser());
}
具体原型对象创建完成 具体原型对象复制成功 对象 c1 和 c2 是用一个对象?false 对象 c1 和 c2 内部的 user 是同一个对象?true 对象 c1.user User(name=贝吉塔, age=19) 对象 c2.user User(name=贝吉塔, age=19)
可以看到浅拷贝中的对象是指向的同一个引用。
如果有需求场景中不允许共享同一对象,那么就需要使用深拷贝,如果想要进行深拷贝需要使用到对象序列化流 (对象序列化之后,再进行反序列化获取到的是不同对象)。
@Test
public void testCloen2() throws Exception {
ConcretePrototypeA c1 = new ConcretePrototypeA();
c1.setUser(new User("卡卡罗特", 18));
//创建序列化输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("c.txt"));
//将 c1 写入到文件当中
oos.writeObject(c1);
oos.close();
//创建对象序列化输入流
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("c.txt"));
//读取对象
ConcretePrototypeA c2 = (ConcretePrototypeA) ois.readObject();
c2.user.setName("贝吉塔");
c2.user.setAge(19);
System.out.println("对象 c1 和 c2 是用一个对象?" + (c1 == c2));
System.out.println("对象 c1 和 c2 内部的 user 是同一个对象?" + (c1.user == c2.user));
System.out.println("对象 c1.user "+c1.getUser());
System.out.println("对象 c2.user "+c2.getUser());
}
具体原型对象创建完成 具体原型对象复制成功 对象 c1 和 c2 是用一个对象?false 对象 c1 和 c2 内部的 user 是同一个对象?true 对象 c1.user User(name=贝吉塔, age=19) 对象 c2.user User(name=贝吉塔, age=19)
利用 Cloenable 接口实现进行克隆有些许的麻烦,所以一般推荐使用 ApacheCommons与springframework 来实现对象原型的克隆
- 浅克隆: BeanUtils.cloneBean(Objectobj);BeanUtils.copyProperties(S,T);
- 深克隆: SerializationUtils.clone(T object);
BeanUtils 是利用反射原理获得所有类可见的属性和方法,然后复制到 target 类。 SerializationUtils.clone() 就是使用我们的前面讲的序列化实现深克隆,当然你要把要克隆的类实现Serialization 接口。
原型模式应用实例
public static void main(String[] args) throws CloneNotSupportedException {
//模拟邮件发送
int i = 0;
//把模板定义出来,数据是从数据库获取的
Mail mail = new Mail(new AdvTemplate());
mail.setTail("xxx银行版权所有");
while (i < MAX_COUNT) {
//下面是每封邮件不同的地方
Mail cloneMail = mail.clone();
cloneMail.setAppellation(" 先生 (女士)");
Random random = new Random();
int num = random.nextInt(9999999);
cloneMail.setReceiver(num + "@" + "liuliuqiu.com");
//发送 邮件
sendMail(cloneMail);
i++;
}
}
原型模式的优点:
- 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程, 通 过复制一个已有实例可以提高新实例的创建效率.
比如,在 AI 系统中,我们经常需要频繁使用大量不同分类的数据模型 文件,在对这一类文件建立对象模型时,不仅会长时间占用 IO 读写资 源,还会消耗大量 CPU 运算资源,如果频繁创建模型对象,就会很容 易造成服务器 CPU 被打满而导致系统宕机。通过原型模式我们可以很 容易地解决这个问题,当我们完成对象的第一次初始化后,新创建的对 象便使用对象拷贝(在内存中进行二进制流的拷贝),虽然拷贝也会消耗一定资源,但是相比初始化的外部读写和运算来说,内存拷贝消耗会 小很多,而且速度快很多
- 原型模式提供了简化的创建结构,工厂方法模式常常需要有一个与产品类等 级结构相同的工厂等级结构(具体工厂对应具体产品),而原型模式就不需要这 样,原型模式的产品复制是通过封装在原型类中的克隆方法实现的,无须专门 的工厂类来创建产品.
- 可以使用深克隆的方式保存对象状态,使用原型模式将对象复制一份并将其 状态保存起来,以便在需要的时候使用,比如恢复到某一历史状态,可以辅助实 现撤销操作.
在某些需要保存历史状态的场景中,比如,聊天消息、上线发布流程、 需要撤销操作的程序等,原型模式能快速地复制现有对象的状态并留存 副本,方便快速地回滚到上一次保存或最初的状态,避免因网络延迟、 误操作等原因而造成数据的不可恢复。
原型模式缺点
需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对 已有的类进行改造时需要修改源代码,违背了开闭原则.
使用场景 原型模式常见的使用场景有以下六种。 资源优化场景。也就是当进行对象初始化需要使用很多外部资源时,比如, IO 资源、数据文件、CPU、网络和内存等。 复杂的依赖场景。 比如,F 对象的创建依赖 A,A 又依赖 B,B 又依赖 C…… 于是创建过程是一连串对象的 get 和 set。 性能和安全要求的场景。 比如,同一个用户在一个会话周期里,可能会反 复登录平台或使用某些受限的功能,每一次访问请求都会访问授权服务器进 行授权,但如果每次都通过 new 产生一个对象会非常烦琐,这时则可以使 用原型模式。 同一个对象可能被多个修改者使用的场景。 比如,一个商品对象需要提供 给物流、会员、订单等多个服务访问,而且各个调用者可能都需要修改其值 时,就可以考虑使用原型模式。 需要保存原始对象状态的场景。 比如,记录历史操作的场景中,就可以通 过原型模式快速保存记录。