天天看点

设计模式(3)之原型模式

设计模式(3)之原型模式

大家在日常开发的过程中是否遇到过如下的烦恼:

  • 新建一个对象太复杂了,而且创建的对象大多数属性都是一致的,只有个别字段值不同
  • 数据库读到数据后,在service层处理的时候需要不停的set/get,显得很费力
  • 业务扩展,某个实体需要新增字段,需要在其他涉及该实体赋值的对象需要补充set/get

上面的问题都太常见,又太痛苦了,那么怎么解决这个问题呢?我们的原型设计模式闪亮登场!

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

顾名思义就是根据已有对象,复制一个新对象。

一、浅克隆

典型的原型模式需要我们自己创建一个Prototype接口,并定义一个clone接口,将需要复制的对象的类实现该接口,并重写clone方法,在clone方法中实现具体的细节。但是java给我们提供了一个非常好的解决方法,这里只介绍java给我们提供的clone方式。

先创建一个类,叫MingRen,它有性别、年龄、技能列表、宠物四个属性:

public class MingRen implements Cloneable {

    private String sex;
    private int age;
    private List<String> skills;
    private Pet pet;

    //为节省篇幅set/get方法略
    ...

    @Override
    public String toString() {
        return "MingRen{" +
                "sex='" + sex + '\'' +
                ", age=" + age +
                ", skills=" + skills +
                ", pet=" + pet +
                '}';
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
           

其中宠物类如下:

public class Pet {
    private String name;
    private String type;
    private List<String> skills;

    ...

    @Override
    public String toString() {
        return "Pet{" +
                "name='" + name + '\'' +
                ", type='" + type + '\'' +
                ", skills=" + skills +
                '}';
    }
}
           

然后我们在测试类中先初始化一个鸣人:

public class PrototypeTest {
    public static void main(String[] args) {
        MingRen mingRen = new MingRen();
        mingRen.setSex("male");
        mingRen.setAge(15);

        List<String> mingrenSkills = new ArrayList<>();
        mingrenSkills.add("螺旋丸");
        mingrenSkills.add("影分身");
        mingRen.setSkills(mingrenSkills);

        Pet hamaji = new Pet();
        hamaji.setName("蛤蟆吉");
        hamaji.setType("蛤蟆");
        List<String> hamajiSkills = new ArrayList<>();
        hamajiSkills.add("找人");
        hamajiSkills.add("搞笑");
        hamaji.setSkills(hamajiSkills);

        mingRen.setPet(hamaji);

        System.out.println("没克隆前的源对象:" + mingRen);
    }
}
           

打印的结果如下:

没克隆前的源对象:MingRen{sex='male', age=15, skills=[螺旋丸, 影分身], pet=Pet{name='蛤蟆吉', type='蛤蟆', skills=[找人, 搞笑]}}
           

可以看到,这个对象的创建已经有一定的复杂性了,既要考虑鸣人自己的技能,还得考虑他召唤出来的召唤兽的技能。而一旦鸣人释放影分身之术,将会有大量的鸣人1、2、3出现,每一个都是真的,总不能每个都这么创建一个吧。于是呼,只需要让鸣人类实现Cloneable接口,并重写clone方法,调用并返回其父类的clone方法,就可以调用clone方法复制这个对象了。

测试代码新增clone方法如下:

try{
    MingRen mingRen1 = (MingRen) mingRen.clone();
    mingRen1.setSex("female");
    mingRen1.getSkills().add("手里剑");
    System.out.println("克隆后的源对象:" + mingRen);
    System.out.println("克隆对象:" + mingRen1);
    System.out.println(mingRen.getSkills() == mingRen1.getSkills());
    System.out.println(mingRen.getPet() == mingRen1.getPet());
}catch (CloneNotSupportedException e){
    e.printStackTrace();
}
           

看看输出结果:

没克隆前的源对象:MingRen{sex='male', age=15, skills=[螺旋丸, 影分身], pet=Pet{name='蛤蟆吉', type='蛤蟆', skills=[找人, 搞笑]}}
克隆后的源对象:MingRen{sex='male', age=15, skills=[螺旋丸, 影分身, 手里剑], pet=Pet{name='蛤蟆吉', type='蛤蟆', skills=[找人, 搞笑]}}
克隆对象:MingRen{sex='female', age=15, skills=[螺旋丸, 影分身, 手里剑], pet=Pet{name='蛤蟆吉', type='蛤蟆', skills=[找人, 搞笑]}}
true
true
           

先看看克隆对象,鸣人释放色诱之术影分身可以看成是女的。只修改了sex属性,并且该分身为了保护自己新增了一个手里剑技能,结果看起来很完美,所有属性一个不落的复制到了。

接下来再看看对克隆后的对象进行修改,会发现源对象也被修改了。说明这个克隆对象的方法是存在一定局限性的。只有基本类型和String类型可以真复制,其他的引用类型复制的都是其引用地址。如果需要复制对象属性而不是引用则需要用到深克隆的技术。

在将深克隆之前,先介绍一个spring为我们提供的BeanUtils里的copyProperties方法,api很简单只需要传入source对象及target对象即可。样例代码如下:

MingRen mingRen3 = new MingRen();
BeanUtils.copyProperties(mingRen, mingRen3);
           

但是这样复制出来的仍然是浅克隆,用来作为赋值非常方便,还可以选择忽略哪些字段不复制,更加详细的将会在spring源码解析中讲解。

二、深克隆

1、利用序列化来实现深克隆,将MingRen及Pet实现Serializable接口,并补充如下代码:

@Override
protected Object clone(){
    return deepClone();
}

private Object deepClone(){
    try{
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(this);

        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        MingRen mingRenCopy = (MingRen) ois.readObject();

        ois.close();
        bis.close();
        oos.close();
        bos.close();

        return mingRenCopy;
    }catch (Exception e){
        e.printStackTrace();
        return null;
    }
}
           

再次运行测试类,将会得到如下结果:

没克隆前的源对象:MingRen{sex='male', age=15, skills=[螺旋丸, 影分身], pet=Pet{name='蛤蟆吉', type='蛤蟆', skills=[找人, 搞笑]}}
克隆后的源对象:MingRen{sex='male', age=15, skills=[螺旋丸, 影分身], pet=Pet{name='蛤蟆吉', type='蛤蟆', skills=[找人, 搞笑]}}
克隆对象:MingRen{sex='female', age=15, skills=[螺旋丸, 影分身, 手里剑], pet=Pet{name='蛤蟆吉', type='蛤蟆', skills=[找人, 搞笑]}}
false
false
           

很显然这样的克隆确保了每一个引用对象都是新的。细心的人应该会问,为什么都是引用类型,我只给Pet实现序列化呢?查看源码就知道了,ArrayList本身已经实现了序列化,因此利用序列化及反序列化是完全可行的。

2、利用fastjson复制对象

这个也是项目中经常用到的,只需要引入fastjson的包,直接调用其api即可,代码如下:

//利用FastJson复制对象
String json = JSON.toJSONString(mingRen);
MingRen mingRen2 = JSON.parseObject(json, MingRen.class);
mingRen2.setSex("female");
mingRen2.getSkills().add("手里剑");
System.out.println("克隆后的源对象:" + mingRen);
System.out.println("克隆对象:" + mingRen2);
System.out.println(mingRen.getSkills() == mingRen2.getSkills());
System.out.println(mingRen.getPet() == mingRen2.getPet());
           

输出结果如下:

没克隆前的源对象:MingRen{sex='male', age=15, skills=[螺旋丸, 影分身], pet=Pet{name='蛤蟆吉', type='蛤蟆', skills=[找人, 搞笑]}}
克隆后的源对象:MingRen{sex='male', age=15, skills=[螺旋丸, 影分身], pet=Pet{name='蛤蟆吉', type='蛤蟆', skills=[找人, 搞笑]}}
克隆对象:MingRen{sex='female', age=15, skills=[螺旋丸, 影分身, 手里剑], pet=Pet{name='蛤蟆吉', type='蛤蟆', skills=[找人, 搞笑]}}
false
false
           

可以看出,复制出来的对象也是完全ok的

三、原型模式与单例模式

可以看出,原型模式和单例模式是完全相悖的。那么如果要保持单例模式,那么还需要在单例模式里加上一些限制。

1、不实现Cloneable接口

不实现该接口,也就不用重写clone方法,自然就不会存在克隆。

2、重写clone方法

如果已经实现了,那么久重写clone方法,让其直接放回单例的实例,或者直接返回getInstance方法也是非常方便的

小结:原型模式是非常简单的一种设计模式,但是在实际项目开发中确实使用率相当高的一种模式。或许你已经在使用了,只是还没有意识到罢了!

继续阅读