设计模式简讲

16. 原型模式

2018-07-03  本文已影响1人  Next_吴思成

定义

原型模式(Prototype Pattern)的定义如下:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

通俗理解

我们生活中经常会用到复印机,用来复印书籍、简历、身份证、银行卡... ...但是,有没有想过,为什么我们书籍、简历这些,要选择复印,而不是重新排版,打印一份呢?(在讨论一些理所当然的东西的时候,总会有一股哲学的味道)。你可以说,这很理所当然呀!重新排版还得消耗人力物力,反正书籍、简历这些都是一样的,直接复印一份不就行了么?

原型模式,就是复印简历的过程,简历都差不多,选择以前的简历进行复印,那么可以节省排版、打印的人力物力。在程序中也是一样,创建差不多的对象,我们可以通过new对象来创建,这样的一种方式会导致new出对象之后,还要对它们的属性进行setter,然后才能够使用。如果我们使用原型模式,对旧的对象进行复制,然后修改它们不同的属性,这一方面可以避免了大量的setter;另外一个方面是节约了资源,创建对象更快,更节省时间(《JVM——Java关键字背后的故事》会有详细的比较与原理,未写)。

示例

业务为复制简历。

渣渣程序

不按照原型模式来,我们可以写出这样的程序:

简历

public class CV {
    /**
     * 名字
     */
    private String name;
    /**
     * 年龄
     */
    private int age;
    /**
     * 技能
     */
    private String skill;
    /**
     * 住址
     */
    private Address address;
   
    // setter和getter省略
}

住址

public class Address {
    /**
     * 国家
     */
    private String country;
    /**
     * 省
     */
    private String province;
    /**
     * 市
     */
    private String city;
    /**
     * 街道
     */
    private String street;
    //setter和getter省略
}

调用方

public class Main {
    public static void main(String[] args) {
        //Jack的简历1
        Address jackAddr1 = new Address();
        jackAddr1.setCountry("China");
        jackAddr1.setProvince("Guangdong");
        jackAddr1.setCity("Shenzhen");
        jackAddr1.setStreet("Baoan");

        CV jack1 = new CV();
        jack1.setName("Jack");
        jack1.setAge(24);
        jack1.setSkill("coding");
        jack1.setAddress(jackAddr1);

        //后来Jack搬家,搬到西乡
        Address jackAddr2 = new Address();
        jackAddr2.setCountry("China");
        jackAddr2.setProvince("Guangdong");
        jackAddr2.setCity("Shenzhen");
        jackAddr2.setStreet("Xixiang");

        CV jack2 = new CV();
        jack2.setName("Jack");
        jack2.setAge(24);
        jack2.setSkill("coding");
        jack2.setAddress(jackAddr2);
    }
}

可怜🤕的Jack,因为家搬到西乡了,就要重新修改一份简历,真是费时费力。

优化

类图

image

通过这个类图,就可以知道,最关键的是clone方法。因此,原型模式,最关键的是写好clone方法。

程序

浅克隆

Java里面有一个声明式的接口Cloneable,这个接口本身没有写任何的方法,只是告诉JVM,该接口的实现类需要开放“克隆”功能,而且在Object当中,clone是一个native方法,表示是JVM直接提供的方法(《JVM——Java关键字背后的故事》会有详细的比较与原理,未写),我们实现这个接口,并重写Object的clone方法就可以了完成对象的拷贝了。

简历

public class CV implements Cloneable{
    /**
     * 名字
     */
    private String name;
    /**
     * 年龄
     */
    private int age;
    /**
     * 技能
     */
    private String skill;
    /**
     * 住址
     */
    private Address address;
    /**
     * 等级
     */
    private Integer level;
  
    // setter和getter省略... ...
 
    public CV clone() {
        Object cloneCV = null;
        try {
            cloneCV = super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return (CV)cloneCV;
    }
}

地址

public class Address{
    /**
     * 国家
     */
    private String country;
    /**
     * 省
     */
    private String province;
    /**
     * 市
     */
    private String city;
    /**
     * 街道
     */
    private String street;
}

调用方

public class Main {
    public static void main(String[] args) {
        //Jack的简历1
        Address jackAddr1 = new Address();
        jackAddr1.setCountry("China");
        jackAddr1.setProvince("Guangdong");
        jackAddr1.setCity("Shenzhen");
        jackAddr1.setStreet("Baoan");

        CV jack1 = new CV();
        jack1.setName("Jack");
        jack1.setAge(24);
        jack1.setSkill("coding");
        jack1.setLevel(1);
        jack1.setAddress(jackAddr1);

        //后来Jack搬家,搬到西乡
        CV jack2 = jack1.clone();
        jack2.getAddress().setStreet("Xixiang");
        jack2.setAge(26);
        jack2.setName("Mack");
        jack2.setLevel(3);

        System.out.println(jack1.getAddress().getStreet());//Xixiang
        System.out.println(jack2.getAddress().getStreet());//Xixiang
        System.out.println(jack1.getAge());//24
        System.out.println(jack2.getAge());//26
        System.out.println(jack1.getName());//Jack
        System.out.println(jack2.getName());//Mack
        System.out.println(jack1.getLevel());//1
        System.out.println(jack2.getLevel());//3
    }
}

通过上面的方式,就可以对对象进行拷贝。很不幸的是,super.clone()只是对对象的浅克隆,只有基本类型(int,boolean等)和String会被复制新的一份,而引用类型(Integer,类,接口,数组等),还是会指向原来的内存地址,造成Jack搬到Xixiang后,第一份简历和第二份简历都被改成Xixiang

深克隆

如果我们也需要把引用类型的属性也复制一份,我们应该怎么改?

序列化,反序列化

public CV clone() {
        CV cv = null;
        try {
            // 将对象写入到流
            FileOutputStream fos = new FileOutputStream("clone.out");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(this);
            // 从流中写入到对象
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("clone.out"));
            cv = (CV) ois.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return cv;
    }
}

逐个属性克隆

public CV clone() {
        CV cv = null;
        try {
            cv = (CV) super.clone();
            cv.address = this.address.clone();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return cv;
    }

优点

  1. 直接在内存中创建对象,提高创建对象的效率
  2. 不使用构造器创建对象,减少了束缚
  3. 可以保存对象的状态,可应用于对象的撤销操作

缺点

  1. 需要克隆对象以及引用的类型都实现clone方法,否则用不了。如果引用嵌套比较深,那么就需要一层一层地去修改。
  2. 不使用构造器创建对象,减少了束缚

应用场景

  1. 资源优化,类的初始化需要很多资源的,包含数据和硬件资源
  2. 性能和安全要求比较高
  3. 一个对象多个修改者,拷贝出备份的让修改者去修改

注意事项

  1. clone()无需调用构造器就能创建对象
  2. final修饰的属性无法使用clone()方法
  3. 最好别用

https://www.jianshu.com/p/51558afcd9c0

上一篇 下一篇

猜你喜欢

热点阅读