設(shè)計(jì)模式(四):原型模式
什么是原型模式?為什么要使用原型模式?
前兩天面試了一個(gè)95年碩士畢業(yè)的小姐姐,在杭州某大廠工作了兩年,最近想回家鄉(xiāng)發(fā)展
對(duì)于兩年以上工作經(jīng)驗(yàn)的候選人,我都會(huì)問(wèn)一些和設(shè)計(jì)模式相關(guān)的面試題
不得不面對(duì)一個(gè)現(xiàn)實(shí),大部分候選人對(duì)設(shè)計(jì)模式都沒有很深入的理解,回答的并不出彩
當(dāng)我對(duì)這個(gè)小姐姐提出這兩個(gè)問(wèn)題時(shí),也沒抱有很高的期望。沒想到小姐姐的回答很讓人意外,甚至可以說(shuō)是讓我對(duì)原型模式有了更深刻的理解
為什么要使用原型模式
假如有一個(gè)類,命名為 A 。A 類里面有兩個(gè)屬性,分別是 x 和 y ,并為這兩個(gè)屬性提供對(duì)應(yīng)的 get 和 set 方法

將這個(gè)類的實(shí)體對(duì)象 a 作為 test 方法的參數(shù)

要求在 test 方法內(nèi)利用 a 對(duì)象的某些屬性進(jìn)行一些業(yè)務(wù)邏輯處理,但不能改變 a 對(duì)象的原有屬性
我們進(jìn)行第一次嘗試:聲明一個(gè)新的對(duì)象 a1 ,并把 a 賦值給它。讓 test 方法利用 a1 對(duì)象的屬性進(jìn)行業(yè)務(wù)邏輯處理
public static void test(A a) {
A a1 = a;
System.out.println("test方法開始業(yè)務(wù)邏輯處理");
a1.setX(1);
}
我們來(lái)驗(yàn)證一下是否會(huì)影響到 a 對(duì)象的屬性
public static void main(String[] args) {
A a = new A();
a.setX(0);
System.out.println("調(diào)用test方法前x=" + a.getX());
test(a);
System.out.println("調(diào)用test方法后x=" + a.getX());
}
輸出結(jié)果為

從輸出結(jié)果來(lái)看,test 方法改變了 a 對(duì)象的屬性,不符合要求。所以,第一次嘗試失敗
其實(shí)也不難理解,我們都知道 JVM 加載對(duì)象后會(huì)給對(duì)象分配內(nèi)存空間
加載完 a 之后,給 a 分配一個(gè)空間

在加載a1 的時(shí)候,因?yàn)?a1 是將 a 的值賦值給了 a1 ,所以在給 a1 分配空間時(shí),只是把 a1 的引用指向了 a 所在的內(nèi)存地址,并沒有給 a1 分配獨(dú)立的內(nèi)存空間

所以修改 a1 對(duì)象的屬性時(shí),a 對(duì)象也會(huì)被改變
我們調(diào)整思路進(jìn)行第二次嘗試:重新 new 一個(gè)新對(duì)象 a2 ,把 a 對(duì)象的所有屬性值賦值給 a2 。test 方法利用 a2 對(duì)象進(jìn)行業(yè)務(wù)邏輯處理
public static void test(A a) {
A a2 = new A();
a2.setX(a.getX());
a2.setY(a.getY());
System.out.println("test方法開始業(yè)務(wù)邏輯處理");
a2.setX(1);
a2.setY(2);
}
同樣來(lái)驗(yàn)證一下是否會(huì)影響到 a 對(duì)象的屬性
public static void main(String[] args) {
A a = new A();
a.setX(0);
a.setY(0);
System.out.println("調(diào)用test方法前x=" + a.getX() + ",y=" + a.getY());
test(a);
System.out.println("調(diào)用test方法后x=" + a.getX() + ",y=" + a.getY());
}
輸出結(jié)果為

這次的輸出結(jié)果顯示,test 方法并沒有改變 a 對(duì)象的屬性,符合要求
但是,有一個(gè)問(wèn)題
- 如果
a不是一個(gè)具體的實(shí)例,而是一個(gè)抽象類或者接口。抽象類或者接口是不能被new的,該怎么辦?
這時(shí)候就要使用到 原型模式 來(lái)解決我們的問(wèn)題了
原型模式
原型模式定義
「原型模式」可以讓你復(fù)制或克隆一個(gè)已有對(duì)象,而又無(wú)需使你的代碼依賴這個(gè)對(duì)象所屬的類
通過(guò)定義我們可以提取出來(lái)兩個(gè)關(guān)鍵信息
第一,原型模式主要作用是復(fù)制或克隆一個(gè)已有對(duì)象
第二,去復(fù)制這個(gè)對(duì)象時(shí)不需要依賴這個(gè)對(duì)象所屬的類
這句話很有意思,想要?jiǎng)?chuàng)建一個(gè)對(duì)象但是不用依賴這個(gè)對(duì)象所屬的類,這要怎么實(shí)現(xiàn)?
答案就是把創(chuàng)建對(duì)象的過(guò)程交給這個(gè)類來(lái)處理
原型模式實(shí)戰(zhàn)
我們用原型模式來(lái)優(yōu)化一下上面的例子
動(dòng)手之前我們需要知道原型模式的設(shè)計(jì)思路
根據(jù)定義可以知道在原型模式中,對(duì)象的創(chuàng)建過(guò)程是交給對(duì)象所屬的類來(lái)處理的,所以這個(gè)類肯定要提供一個(gè)方法,方法的返回值是這個(gè)對(duì)象。通常這個(gè)方法叫 clone() 或 copy()
套用到上面例子的 A 類中,需要在 A 類里面提供一個(gè) clone() 方法,在方法中創(chuàng)建一個(gè)當(dāng)前對(duì)象并返回

在 test 方法中利用 clone() 來(lái)獲取一個(gè) a3 對(duì)象去執(zhí)行業(yè)務(wù)邏輯
public static void test(A a) {
A a3 = a.clone();
System.out.println("test方法開始業(yè)務(wù)邏輯處理");
a3.setX(1);
}
再驗(yàn)證一下是否改變了 a 對(duì)象的屬性 
從輸出結(jié)果可以看到是沒有改變 a 對(duì)象的屬性的
那我們?cè)賮?lái)解決上面例子中遇到的問(wèn)題,假如 A 是一個(gè)抽象類,該怎么去創(chuàng)建這個(gè)對(duì)象
其實(shí)也很簡(jiǎn)單,抽象類中是可以有抽象方法的。把 clone() 方法定義為抽象方法,讓子類去實(shí)現(xiàn)它

假如 A 有兩個(gè)子類,分別是 SubA1 和 SubA2。兩個(gè)子類分別繼承 A 抽象類,并實(shí)現(xiàn) clone()抽象方法

在 test 中還是使用 a.clone() 就可以得到一個(gè)新的對(duì)象,而且不會(huì)影響到原有的 a 對(duì)象
這就用 原型模式 對(duì)上面的例子完成了優(yōu)化
深拷貝、淺拷貝
在java中,默認(rèn) Object 類是所有類的父類,在 Object 中有一個(gè) clone() 方法

它是java默認(rèn)提供的用來(lái)復(fù)制對(duì)象的方法,這個(gè)方法是 native 修飾的,說(shuō)明它是對(duì)操作系統(tǒng)的底層直接調(diào)用的,在理論上,用它來(lái)復(fù)制對(duì)象性能會(huì)更好
所以,我們可以使用 java.lang.Object#clone() 來(lái)實(shí)現(xiàn)原型模式,總共分為兩步
- 被復(fù)制的類需要實(shí)現(xiàn)
Cloneable這個(gè)接口類。這個(gè)接口類里面是沒有任何一個(gè)方法的,只是起到一個(gè)標(biāo)記作用,也可以理解成一種約定(「約定大于配置」) - 被復(fù)制的類需要重寫
Object中的clone()方法
下面我們就來(lái)優(yōu)化一下 A 類

這樣寫出的原型模式,在理論上執(zhí)行效率更高。看似完美,實(shí)則不然
假如 A 類里面有一個(gè) ArrayList 屬性

我們來(lái)看一下,在 clone 完 a 后得到 a4,改變 a4的 list 屬性,會(huì)不會(huì)對(duì) a 造成影響

輸出結(jié)果為

在修改 a4 對(duì)象時(shí),也改變了 a 對(duì)象的屬性值,這不是我們期望的結(jié)果
這是因?yàn)椋?code>Object 在 clone 時(shí)只會(huì)對(duì)基礎(chǔ)類型的數(shù)據(jù)進(jìn)行拷貝,引用類型的數(shù)據(jù)并沒有真正的拷貝,而是把引用指針指向了這個(gè)數(shù)據(jù)在內(nèi)存中的地址(還記得上文中 a 和 a1 指向同一個(gè)內(nèi)存地址的例子嗎)
這種只拷貝基礎(chǔ)數(shù)據(jù)類型的行為,我們稱之為 淺拷貝。既可以拷貝基礎(chǔ)數(shù)據(jù)類型,又可以拷貝引用數(shù)據(jù)類型的行為,我們稱之為深拷貝
在原型模式中,我們應(yīng)該使用 深拷貝 來(lái)復(fù)制對(duì)象。
要實(shí)現(xiàn)深拷貝,「需要這個(gè)引用類型的數(shù)據(jù)所屬的類也實(shí)現(xiàn) Cloneable 接口,并且重寫 Object 類的 clone() 方法」
在本例子中,引用類型所屬的類是 ArrayList,它本身已經(jīng)實(shí)現(xiàn)了 Cloneable 接口,并重寫了 Object 類的 clone() 方法。
我們只需要在 A 類的 clone() 方法中調(diào)用 ArrayList 的 clone() 方法即可

這樣就基于 深拷貝 完成了原型模式
總結(jié)
「原型模式」也叫「克隆模式」,它屬于設(shè)計(jì)模式三大類型中的創(chuàng)建型模式
在你需要復(fù)制一個(gè)對(duì)象,而又不希望改變?cè)袑?duì)象的時(shí)候可以考慮使用原型模式來(lái)實(shí)現(xiàn)
在實(shí)現(xiàn)原型模式時(shí),引用類型數(shù)據(jù)的復(fù)制要基于 深拷貝 ,否則會(huì)影響到被拷貝的 原型
在 Spring 生態(tài)下,對(duì)象的創(chuàng)建基本都由 IOC 來(lái)實(shí)現(xiàn),原型模式 好像沒有多少用武之地
但是,用的少不代表沒用。我們?cè)趯W(xué)習(xí)設(shè)計(jì)模式時(shí),目的不僅僅在于要學(xué)會(huì)設(shè)計(jì)模式,而是要學(xué)會(huì)設(shè)計(jì)模式使用的設(shè)計(jì)思想
學(xué)會(huì)這種思想,沉淀為自己的思路,在工作中能實(shí)現(xiàn)舉一反三,才能無(wú)往不利
-- 以上內(nèi)容來(lái)自公眾號(hào) 「赫連小伍」 ,轉(zhuǎn)載請(qǐng)注明出處
浙公網(wǎng)安備 33010602011771號(hào)