设计模式-原型模式

91 阅读7分钟

设计模式-原型模式

原型模式 prototypePattern

工厂模式

原型模式原理与介绍

原型模式其实就是用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象

比如在《西游记》当中的猴哥用自己的猴毛克隆了多个与自己一模一样的本体,这就是对象原型,而猴哥(克隆本体)就是原型,变出千千万万个克隆的对象。

原型模式主要解决的问题

如果说一个对象内部是足够复杂,对象足够大,内部的数据是要通过大量的精密计算才能得到的,或者说需要通过 RPC 的接口或者数据库等比较慢的 IO 中获取,这种情况我们就需要用到原型模式。

直接从原有的现成对象模型当中克隆一份,就不需要每一次都需要创建一个新的对象,进行一些费事的操作,可以节省大量的时间与资源。

原型模式的角色

抽象原型类(Prototype):它是声明克隆方法的接口,是所有具体原型类的公共父类,它可以是抽象类也可以是接口

具体原型类(ConcretePrototype):实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象

客户类(Client):在客户类中,让一个原型对象克隆自身从而创建一个新的对象,由于客户类针对抽象原型类Prototype编程.因此用户可以根据需要选择具体原型类,系统具有较好的扩展性,增加或者替换具体原型类都比较方便 。

image-20230519095441702

深克隆与浅克隆

  • 浅克隆

    image-20230519104633381

  • 深克隆

    image-20230519104640551

在 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 接口。

原型模式应用实例

image-20230520001356379

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++;
        }
    }

原型模式的优点:

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

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

  1. 原型模式提供了简化的创建结构,工厂方法模式常常需要有一个与产品类等 级结构相同的工厂等级结构(具体工厂对应具体产品),而原型模式就不需要这 样,原型模式的产品复制是通过封装在原型类中的克隆方法实现的,无须专门 的工厂类来创建产品.
  1. 可以使用深克隆的方式保存对象状态,使用原型模式将对象复制一份并将其 状态保存起来,以便在需要的时候使用,比如恢复到某一历史状态,可以辅助实 现撤销操作.

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

原型模式缺点

需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对 已有的类进行改造时需要修改源代码,违背了开闭原则.

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