第四章 4.5 原型模式

分类:01_设计模式

标签:

4.5.1 原型模式介绍

定义: 原型模式(Prototype Design Pattern)用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象。

西游记中的孙悟空 拔毛变小猴,孙悟空这种根据自己的形状复制出多个身外化身的技巧,在面向对象软件设计领域被称为原型模式.孙悟空就是原型对象.

54.jpg

原型模式主要解决的问题

4.5.2 原型模式原理

原型模式包含如下角色:

55.jpg

4.5.3 深克隆与浅克隆

根据在复制原型对象的同时是否复制包含在原型对象中引用类型的成员变量 这个条件,原型模式的克隆机制分为两种,即浅克隆(Shallow Clone)和深克隆(Deep Clone)

1) 什么是浅克隆

被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象(克隆对象与原型对象共享引用数据类型变量)。

56.jpg

2) 什么是深克隆

除去那些引用其他对象的变量,被复制对象的所有变量都含有与原来的对象相同的值。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深复制把要复制的对象所引用的对象都复制了一遍。

57.jpg

Java中的Object类中提供了 clone() 方法来实现浅克隆。需要注意的是要想实现克隆的Java类必须实现一个标识接口 Cloneable ,来表示这个Java类支持被复制.

Cloneable接口是上面的类图中的抽象原型类,而实现了Cloneable接口的子实现类就是具体的原型类。代码如下:

3) 浅克隆代码实现:


public class ConcretePrototype implements Cloneable {

   public ConcretePrototype() {
       System.out.println("具体的原型对象创建完成!");
   }

   @Override
   protected ConcretePrototype clone() throws CloneNotSupportedException {
       System.out.println("具体的原型对象复制成功!");
       return (ConcretePrototype)super.clone();
   }
}

测试


   @Test
   public void test01() throws CloneNotSupportedException {
       ConcretePrototype c1 = new ConcretePrototype();
       ConcretePrototype c2 = c1.clone();

       System.out.println("对象c1和c2是同一个对象?" + (c1 == c2));
   }

4) 深克隆代码实现

在ConcretePrototype类中添加一个对象属性为Person类型


public class ConcretePrototype implements Cloneable {

   private Person person;

   public Person getPerson() {
       return person;
   }

   public void setPerson(Person person) {
       this.person = person;
   }

   void show(){
       System.out.println("嫌疑人姓名: " +person.getName());
   }

   public ConcretePrototype() {
       System.out.println("具体的原型对象创建完成!");
   }

   @Override
   protected ConcretePrototype clone() throws CloneNotSupportedException {
       System.out.println("具体的原型对象复制成功!");
       return (ConcretePrototype)super.clone();
   }

}

public class Person {

   private String name;

   public Person() {
   }

   public Person(String name) {
       this.name = name;
   }

   public String getName() {
       return name;
   }

   public void setName(String name) {
       this.name = name;
   }
}

测试


   @Test
   public void test02() throws CloneNotSupportedException {
       ConcretePrototype c1 = new ConcretePrototype();
       Person p1 = new Person();
       c1.setPerson(p1);

       //复制c1
       ConcretePrototype c2 = c1.clone();
       //获取复制对象c2中的Person对象
       Person p2 = c2.getPerson();
       p2.setName("峰哥");

       //判断p1与p2是否是同一对象
       System.out.println("p1和p2是同一个对象?" + (p1 == p2));

       c1.show();
       c2.show();
   }

打印结果

说明: p1与p2是同一对象,这是浅克隆的效果,也就是对具体原型类中的引用数据类型的属性进行引用的复制.

如果有需求场景中不允许共享同一对象,那么就需要使用深拷贝,如果想要进行深拷贝需要使用到对象序列化流 (对象序列化之后,再进行反序列化获取到的是不同对象).  代码如下:


   @Test
   public void test03() throws Exception {

       ConcretePrototype c1 = new ConcretePrototype();
       Person p1 = new Person("峰哥");
       c1.setPerson(p1);

       //创建对象序列化输出流
       ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("c.txt"));

       //将c1对象写到文件中
       oos.writeObject(c1);
       oos.close();

       //创建对象序列化输入流
       ObjectInputStream ois = new ObjectInputStream(new FileInputStream("c.txt"));

       //读取对象
       ConcretePrototype c2 = (ConcretePrototype) ois.readObject();
       Person p2 = c2.getPerson();
       p2.setName("凡哥");

       //判断p1与p2是否是同一个对象
       System.out.println("p1和p2是同一个对象?" + (p1 == p2));

       c1.show();
       c2.show();
   }

打印结果:

注意:ConcretePrototype类和Person类必须实现Serializable接口,否则会抛NotSerializableException异常。


其实现在不推荐大家用Cloneable接口,实现比较麻烦,现在借助Apache Commons或者springframework可以直接实现:

BeanUtils是利用反射原理获得所有类可见的属性和方法,然后复制到target类。SerializationUtils.clone()就是使用我们的前面讲的序列化实现深克隆,当然你要把要克隆的类实现Serialization接口。

4.5.4 原型模式应用实例

模拟某银行电子账单系统的广告信发送功能,广告信的发送都是有一个模板的,从数据库查出客户的信息,然后放到模板中生成一份完整的邮件,然后交给发送机进行发送处理.

58.jpg

发送广告信邮件UML类图

59.jpg

代码实现


/**
* 广告信模板代码
* @author spikeCong
* @date 2022/9/20
**/
public class AdvTemplate {

   //广告信名称
   private String advSubject = "xx银行本月还款达标,可抽iPhone 13等好礼!";

   //广告信内容
   private String advContext = "达标用户请在2022年3月1日到2022年3月30参与抽奖......";

   public String getAdvSubject() {
       return this.advSubject;
   }

   public String getAdvContext() {
       return this.advContext;
   }
}

package com.mashibing.example01;

/**
* 邮件类
* @author spikeCong
* @date 2022/9/20
**/
public class Mail {

   //收件人
   private String receiver;
   //邮件名称
   private String subject;
   //称谓
   private String appellation;
   //邮件内容
   private String context;
   //邮件尾部, 一般是"xxx版权所有"等信息
   private String tail;


   //构造函数
   public Mail(AdvTemplate advTemplate) {
       this.context = advTemplate.getAdvContext();
       this.subject = advTemplate.getAdvSubject();
   }

   public String getReceiver() {
       return receiver;
   }

   public void setReceiver(String receiver) {
       this.receiver = receiver;
   }

   public String getSubject() {
       return subject;
   }

   public void setSubject(String subject) {
       this.subject = subject;
   }

   public String getAppellation() {
       return appellation;
   }

   public void setAppellation(String appellation) {
       this.appellation = appellation;
   }

   public String getContext() {
       return context;
   }

   public void setContext(String context) {
       this.context = context;
   }

   public String getTail() {
       return tail;
   }

   public void setTail(String tail) {
       this.tail = tail;
   }
}

/**
* 业务场景类
* @author spikeCong
* @date 2022/9/20
**/
public class Client {

   //发送信息的是数量,这个值可以从数据库获取
   private static int MAX_COUNT = 6;

   //发送邮件
   public static void sendMail(Mail mail){
       System.out.println("标题: " + mail.getSubject() + "\t收件人: " + mail.getReceiver()
       + "\t..发送成功!");
   }

   public static void main(String[] args) {

       //模拟邮件发送
       int i = 0;

       //把模板定义出来,数据是从数据库获取的
       Mail mail = new Mail(new AdvTemplate());
       mail.setTail("xxx银行版权所有");
       while(i < MAX_COUNT){
           //下面是每封邮件不同的地方
           mail.setAppellation(" 先生 (女士)");
           Random random = new Random();
           int num = random.nextInt(9999999);
           mail.setReceiver(num+"@"+"liuliuqiu.com");
           //发送 邮件
           sendMail(mail);
           i++;
       }
   }
}

60.jpg

上面的代码存在的问题:

代码重构


/**
* 邮件类 实现Cloneable接口,表示该类的实例可以被复制
* @author spikeCong
* @date 2022/9/20
**/
public class Mail implements Cloneable{

   //收件人
   private String receiver;
   //邮件名称
   private String subject;
   //称谓
   private String appellation;
   //邮件内容
   private String context;
   //邮件尾部, 一般是"xxx版权所有"等信息
   private String tail;


   //构造函数
   public Mail(AdvTemplate advTemplate) {
       this.context = advTemplate.getAdvContext();
       this.subject = advTemplate.getAdvSubject();
   }

   public String getReceiver() {
       return receiver;
   }

   public void setReceiver(String receiver) {
       this.receiver = receiver;
   }

   public String getSubject() {
       return subject;
   }

   public void setSubject(String subject) {
       this.subject = subject;
   }

   public String getAppellation() {
       return appellation;
   }

   public void setAppellation(String appellation) {
       this.appellation = appellation;
   }

   public String getContext() {
       return context;
   }

   public void setContext(String context) {
       this.context = context;
   }

   public String getTail() {
       return tail;
   }

   public void setTail(String tail) {
       this.tail = tail;
   }

   @Override
   public Mail clone(){
       Mail mail = null;
       try {
           mail = (Mail)super.clone();
       } catch (CloneNotSupportedException e) {
           e.printStackTrace();
       }
       return mail;
   }
}

/**
* 业务场景类
* @author spikeCong
* @date 2022/9/20
**/
public class Client {

   //发送信息的是数量,这个值可以从数据库获取
   private static int MAX_COUNT = 6;

   //发送邮件
   public static void sendMail(Mail mail){
       System.out.println("标题: " + mail.getSubject() + "\t收件人: " + mail.getReceiver()
       + "\t..发送成功!");
   }

   public static void main(String[] args) {

       //模拟邮件发送
       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++;
       }
   }
}

4.5.5 原型模式总结

原型模式的优点

  1. 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程, 通过复制一个已有实例可以提高新实例的创建效率.

    比如,在 AI 系统中,我们经常需要频繁使用大量不同分类的数据模型文件,在对这一类文件建立对象模型时,不仅会长时间占用 IO 读写资源,还会消耗大量 CPU 运算资源,如果频繁创建模型对象,就会很容易造成服务器 CPU 被打满而导致系统宕机。通过原型模式我们可以很容易地解决这个问题,当我们完成对象的第一次初始化后,新创建的对象便使用对象拷贝(在内存中进行二进制流的拷贝),虽然拷贝也会消耗一定资源,但是相比初始化的外部读写和运算来说,内存拷贝消耗会小很多,而且速度快很多

  2. 原型模式提供了简化的创建结构,工厂方法模式常常需要有一个与产品类等级结构相同的工厂等级结构(具体工厂对应具体产品),而原型模式就不需要这样,原型模式的产品复制是通过封装在原型类中的克隆方法实现的,无须专门的工厂类来创建产品.

  3. 可以使用深克隆的方式保存对象状态,使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用,比如恢复到某一历史状态,可以辅助实现撤销操作.

    在某些需要保存历史状态的场景中,比如,聊天消息、上线发布流程、需要撤销操作的程序等,原型模式能快速地复制现有对象的状态并留存副本,方便快速地回滚到上一次保存或最初的状态,避免因网络延迟、误操作等原因而造成数据的不可恢复。

原型模式缺点

使用场景

原型模式常见的使用场景有以下六种。


修改内容