🌲本文收錄于專欄《源碼中的設計模式》——理論與實戰的完美結合
作者其它優質專欄推薦:
📚《技術專家修煉》——搞技術,進大廠,聊人生三合一專欄
📚《leetcode 300題》——每天一道算法題,進大廠必備
📚《糊塗算法》——從今天起,邁過資料結構和算法這道坎
📚《從實戰學python》——Python的爬蟲,自動化,AI等實戰應用
點選跳轉到文末領取粉絲福利
哈喽,大家好,我是一條~
之前的《白話設計模式》因為工作被擱置,如今再次啟航,并搭配架構源碼解析一起食用,将理論與實戰完美結合。
對設計模式不是很熟悉的同學可以先看一下《23種設計模式的一句話通俗解讀》全面的了解一下設計模式,形成一個整體的架構,再逐個擊破。
今天我們一塊看一下原型模式,屬于簡單且常用的一種。
定義
官方定義
用原型執行個體指定建立對象的種類,并且通過拷貝這個原型來建立新的對象。
通俗解讀
在需要建立重複的對象,為了保證性能,本體給外部提供一個克隆體進行使用。
類似我國的印刷術,省去
new
的過程,通過
copy
的方式建立對象。
結構圖
代碼實作
目錄結構
建議跟着一條學設計模式的小夥伴都建一個工程,并安裝
maven
lombok
依賴和插件。
并建立如下包目錄,便于歸納整理。
pom
如下
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
</dependency>
開發場景
假設一條開發了一個替代
Mybatis
的架構,叫
YitiaoBatis
,每次操作資料庫,從資料庫裡面查出很多記錄,但是改變的部分是很少的,如果每次查資料庫,查到以後把所有資料都封裝一個對象,就會導緻要
new
很多重複的對象,造成資源的浪費。
一條想到一個解決辦法,就是把查過的資料儲存起來,下來查相同的資料,直接把儲存好的對象傳回,也就是緩存的思想。
我們用代碼模拟一下:
1.建立 Yitiao
實體類
Yitiao
/**
* author:一條
*/
@Data
@AllArgsConstructor
public class Yitiao {
private String name;
private Integer id;
private String wechat;
public Yitiao(){
System.out.println("Yitiao對象建立");
}
}
2.建立 YitiaoBatis
類
YitiaoBatis
/**
* author:一條
*/
public class YitiaoBatis {
//緩存Map
private Map<String,Yitiao> yitiaoCache = new HashMap<>();
//從緩存拿對象
public Yitiao getYitiao(String name){
//判斷緩存中是否存在
if (yitiaoCache.containsKey(name)){
Yitiao yitiao = yitiaoCache.get(name);
System.out.println("從緩存查到資料:"+yitiao);
return yitiao;
}else {
//模拟從資料庫查資料
Yitiao yitiao = new Yitiao();
yitiao.setName(name);
yitiao.setId(1);
yitiao.setWechat("公衆号:一條coding");
System.out.println("從資料庫查到資料:"+yitiao);
//放入緩存
yitiaoCache.put(name,yitiao);
return yitiao;
}
}
}
3.編寫測試類
/**
* author:一條
*/
public class MainTest {
public static void main(String[] args) {
YitiaoBatis yitiaoBatis = new YitiaoBatis();
Yitiao yitiao1 = yitiaoBatis.getYitiao("yitiao");
System.out.println("第一次查詢:"+yitiao1);
Yitiao yitiao2 = yitiaoBatis.getYitiao("yitiao");
System.out.println("第二次查詢:"+yitiao2);
}
}
輸出結果
從結果可以看出:
- 對象建立了一次,有點單例的感覺
- 第一次從資料庫查,第二次從緩存查
好像是實作了
YitiaoBatis
架構的需求,思考🤔一下有什麼問題呢?
4.修改對象id
在測試類繼續編寫
//執行後續業務,修改id
yitiao2.setId(100);
Yitiao yitiao3 = yitiaoBatis.getYitiao("yitiao");
System.out.println("第三次查詢:"+yitiao3);
輸出結果
重點看第三次查詢,
id=100?
我們在記憶體修改的資料,導緻從資料庫查出來的資料也跟着改變,出現髒資料。
怎麼解決呢?原型模式正式開始。
5.實作 Cloneable
接口
Cloneable
本體給外部提供一個克隆體進行使用,在緩存中拿到的對象不直接傳回,而是複制一份,這樣就保證了不會髒緩存。
public class Yitiao implements Cloneable{
//……
@Override
protected Object clone() throws CloneNotSupportedException {
return (Yitiao) super.clone();
}
}
修改緩存
//從緩存拿對象
public Yitiao getYitiao(String name) throws CloneNotSupportedException {
//判斷緩存中是否存在
if (yitiaoCache.containsKey(name)){
Yitiao yitiao = yitiaoCache.get(name);
System.out.println("從緩存查到資料:"+yitiao);
//修改傳回
//return yitiao;
return yitiao.clone();
}else {
//模拟從資料庫查資料
Yitiao yitiao = new Yitiao();
yitiao.setName(name);
yitiao.setId(1);
yitiao.setWechat("公衆号:一條coding");
System.out.println("從資料庫查到資料:"+yitiao);
//放入緩存
yitiaoCache.put(name,yitiao);
//修改傳回
//return yitiao;
return yitiao.clone();
}
6.再次測試
不用改測試類,直接看一下結果:
從輸出結果可以看出第三次查詢
id
依然是
1
,沒有髒緩存現象。
基于原型模式的克隆思想,我可以快速拿到和「本體」一模一樣的「克隆體」,而且對象也隻被
new
了一次。
不知道大家是否好奇對象是怎麼被建立出來的,那我們就一起看一下「深拷貝」和「淺拷貝」是怎麼回事。
深拷貝和淺拷貝
定義
深拷貝:不管拷貝對象裡面是基本資料類型還是引用資料類型都是完全的複制一份到新的對象中。
淺拷貝:當拷貝對象隻包含簡單的資料類型比如int、float 或者不可變的對象(字元串)時,就直接将這些字段複制到新的對象中。而引用的對象并沒有複制而是将引用對象的位址複制一份給克隆對象。
好比兩個兄弟,深拷貝是年輕的時候關系特别好,衣服買一樣的,房子住一塊。淺拷貝是長大了都成家立業,衣服可以繼續買一樣的,但房子必須要分開住了。
實作
在代碼上區分深拷貝和淺拷貝的方式就是看引用類型的變量在修改後,值是否發生變化。
淺拷貝
1.通過
clone()
方式的淺拷貝
建立
Age
類,作為
Yitiao
的引用屬性
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Age {
private int age;
}
2.測試1
public static void main(String[] args) throws CloneNotSupportedException {
Yitiao yitiao1 = new Yitiao();
Age age = new Age(1);
yitiao1.setAge(age);
yitiao1.setId(1);
Yitiao clone = yitiao1.clone();
yitiao1.setId(2);
age.setAge(2); //不能new一個age
System.out.println("yitiao1:\n"+yitiao1+"\nclone:\n"+clone);
}
輸出結果
結論:基本類型
id
沒發生改變,引用類型
Age
由于位址指向的同一個對象,值跟随變化。
3.通過構造方法實作淺拷貝
Yitiao.class
增加構造方法
public Yitiao(Yitiao yitiao){
id=yitiao.id;
age=yitiao.age;
}
4.測試2
Yitiao yitiao1 = new Yitiao();
Age age = new Age(1);
yitiao1.setAge(age);
yitiao1.setId(1);
Yitiao clone = new Yitiao(yitiao1); //差别在這
yitiao1.setId(2);
age.setAge(2);
System.out.println("yitiao1:\n"+yitiao1+"\nclone:\n"+clone);
輸出結果
與測試1無異
深拷貝
1.通過對象序列化實作深拷貝
通過層次調用clone方法也可以實作深拷貝,但是代碼量太大。特别對于屬性數量比較多、層次比較深的類而言,每個類都要重寫clone方法太過繁瑣。一般不使用,亦不再舉例。
可以通過将對象序列化為位元組序列後,預設會将該對象的整個對象圖進行序列化,再通過反序列即可完美地實作深拷貝。
Yitiao
和
Age
實作Serializable接口
2.測試
//通過對象序列化實作深拷貝
Yitiao yitiao = new Yitiao();
Age age = new Age(1);
yitiao.setAge(age);
yitiao.setId(1);
ByteArrayOutputStream bos=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(bos);
oos.writeObject(yitiao);
oos.flush();
ObjectInputStream ois=new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
Yitiao clone = (Yitiao) ois.readObject();
yitiao.setId(2);
age.setAge(2);
System.out.println("yitiao:\n"+yitiao+"\nclone:\n"+clone);
輸出結果
結論,引用對象也完全複制一個新的,值不變化。
不過要注意的是,如果某個屬性被
transient
修飾,那麼該屬性就無法被拷貝了。
應用場景
我們說回原型模式。
原型模式在我們的代碼中是很常見的,但是又容易被我們所忽視的一種模式,比如我們常用的的
BeanUtils.copyProperties
就是一種對象的淺拷貝。
看看有哪些場景需要原型模式
- 資源優化
- 性能和安全要求
- 一個對象多個修改者的場景。
- 一個對象需要提供給其他對象通路,而且各個調用者可能都需要修改其值時可以考慮使用原型模式拷貝多個對象供調用者使用。
原型模式已經與 Java 融為渾然一體,可以随手拿來使用。
總結
原型模式應該算是除了單例最簡單的設計模式,但我還是寫了将近4個小時,畫圖,敲代碼,碼字,不知不覺寫了
8000
字。
一篇優質的原創文真的很耗費作者的心血,是以如果感覺寫的還不錯,麻煩給個三連,這對一條來說很重要,也是一條創作下去的動力!
最後
古語雲:乘衆人之智,則無不任也;用衆人之力,則無不勝也。一個人或許可以走的很快,但一群人才能走的更遠。
為此,我制定了抱團生長計劃,每天分享1-3篇優質文章和1道leetcode算法題
如果你剛剛大一,每天堅持學習,你将會至少比别人多看4000篇文章,多刷1200道題,那麼畢業時你的工資就可能是别人的3-4倍。
如果你是職場人,每天提升自己,升職加薪,成為技術專家指日可待。
隻要你願意去奮鬥,始終走在拼搏的路上,那你的人生,最壞的結果,也不過是大器晚成。
點此
加入計劃如果連結被屏蔽,或者有權限問題,可以私聊作者解決。
粉絲專屬福利
📚Java:1.5G學習資料——回複「資料」
📚算法:視訊書籍——回複「算法」
👇 點選下方卡片
關注後回複
關鍵詞領取👇