天天看點

設計模式(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方法也是非常友善的

小結:原型模式是非常簡單的一種設計模式,但是在實際項目開發中确實使用率相當高的一種模式。或許你已經在使用了,隻是還沒有意識到罷了!

繼續閱讀