設計模式 - Prototype 原型模式
前言
在設計模式的系列文章中,我們前面已經(jīng)寫了工廠模式、單列模式、建造者模式,在針對創(chuàng)建型模式中,今天想跟大家分享的是原型模式
其實原型模式在我們的代碼中是很常見的,但是又容易被我們所忽視的一種模式,那么什么是原型模式呢?
原型模式其實就是一種克隆對象的方法,在我們的編碼時候是很常見的,比如我們常用的的BeanUtils.copyProperties就是一種對象的淺copy,其實現(xiàn)在我們實例化對象操作并不是特別耗費性能,所以在針對一些特殊場景我們還是需要克隆那些已經(jīng)實例化的對象的:
- 依賴外部資源或硬件密集型操作,比如數(shù)據(jù)庫查詢,或者一些存在IO操作的場景
- 獲取相同對象在相同狀態(tài)的拷貝從而不需要重復創(chuàng)建獲取狀態(tài)的操作的情況
看下我們的類圖:
在上面的圖中我們可以看出原型模式其實很簡單:
- 第一個是抽象原型(prototype)聲明clone方法,可以是接口可以是基類,在簡單的場景下我們都可以不用基類直接具體類就可以了。
- 第二個就是具體原型類(concreteprototype)實現(xiàn)或者擴展clone方法,當我們在具體的原型類中的對象方法時,就會返回一個基類的抽象原型對象
針對上面理論知識,我們還是實際的舉一個例子吧!
舉例
假設現(xiàn)在我們有這么一種場景,公司搞一場活動有五萬個商品參加此次活動,我們需要從后臺能定時同步每個商品的銷量,方便我們?yōu)楹竺娴幕顒幼錾唐贩治?,我們要怎么處理這個銷量同步問題?
首先在這里銷量和庫存都是屬于熱點數(shù)據(jù),但肯定都是相互隔離的因為庫存是要求實時性很高的,銷量可以允許有短暫延時,只要能保證數(shù)據(jù)能夠最終一致性就行,所以下單的同時我們可以根據(jù)一個MQ去更新我們數(shù)據(jù)庫里的商品銷量。
在我們?nèi)ゲ榭翠N量的時候我們不能每次都是去查DB所以我們可以通過redis緩存來處理,同時我們在緩存中記錄一下我們當前查詢的更新時間。
再次查詢時通過redis數(shù)據(jù)里面的更新時間,作為查詢條件去查詢DB中的更新時間大于我們當前redis中的記錄時間,這樣就減少了SQL的掃表的行數(shù)(更新的數(shù)據(jù)與全量數(shù)據(jù)相比,更新的數(shù)據(jù)量還是占少數(shù)的)
基于上面流程我們開始寫demo了
在這里demo中我們先是創(chuàng)建了一個ItemSold類,以及一個SkuSold類同時ItemSold重寫Cloneable里面的clone方法。然后在最后的測試類mian方法中我調(diào)用了clone方法,copy一個新的商品銷量類。
細心的同學在看結(jié)果的時候不知道有沒有發(fā)現(xiàn)一個問題?在for循環(huán)里面,我分別打印出來的ItemSold 以及 SkuSold對象他們的內(nèi)存地址。
復制出來的SkuSold的內(nèi)存地址居然和原型地址一樣,ItemSold的復制就和原型地址不一樣了,針對這個問題這里我們就要聊聊原型模式的兩種實現(xiàn)淺拷貝和深拷貝了。
- 這里說明一下我們在for循環(huán)里面是做數(shù)據(jù)convert,一般來說我們不會引用底層模型來做返回結(jié)果模型,需要做一層轉(zhuǎn)化,來達到防腐的效果。為了體現(xiàn)深淺拷貝,所以寫的比較簡單,具體還是需要自己根據(jù)實際情況來做。
淺拷貝和深拷貝
- 淺拷貝:當拷貝對象只包含簡單的數(shù)據(jù)類型比如int、float 或者不可變的對象(字符串)時,就直接將這些字段復制到新的對象中。而引用的對象并沒有復制而是將引用對象的地址復制一份給克隆對象
- 深拷貝:不管拷貝對象里面簡單數(shù)據(jù)類型還是引用對象類型都是會完全的復制一份到新的對象中
舉個例子這就好比兩兄弟大家買衣服可以一人一套,然后房子大家住在一套房子里(淺拷貝),當兩個人成家立業(yè)了,房子分開了一人一套互不影響(深拷貝)
看完這張圖,大家也就明白了,上面的demo是一個淺拷貝,那么我們要怎么做才能實現(xiàn)深拷貝呢?
首先我們先來看下 Java的提供的Cloneable 接口
看接口上面的解釋大致可以理解為:
- 一個類實現(xiàn)了Cloneable接口,來實現(xiàn)這個類的clone方法從而可以合法地對該類實例進行按字段復制,假設這個類沒有實現(xiàn)Cloneable接口的實例上調(diào)用Object的clone()方法,則會導致拋出CloneNotSupporteddException異常。
那么我們這里怎么實現(xiàn)深拷貝呢?
第一種:在重寫ItemSold里面的clone方法時,再針對SkuSold也進行一次拷貝 (因為我們這里時List對象,只能是先拿到淺拷貝,再通過淺拷貝的List對象進行遍歷再調(diào)用引用對象的clone方法來實現(xiàn)深拷貝)
這里如果引用對象存在多級情況下我們可能就要考慮用遞歸了實現(xiàn),但是代碼看上去就會復雜很多了。
第二種:通過序列化把對象寫入流中再從流中取出來
針對上面的兩種寫法其實都是可以實現(xiàn)的,但是不管用哪種方式,深拷貝都比淺拷貝花時間和空間,所以還是酌情考慮。其實在現(xiàn)在已經(jīng)有很多針對淺拷貝和深拷貝的工具類
- 深拷貝(deep copy):SerializationUtils
- 淺拷貝(shallow copy):BeanUtils
思考
針對上面的業(yè)務場景我們也可以通過其他的方式統(tǒng)計商品銷量,可以再通過一個MQ去增加銷量的同時再去更新redis緩存,但是需要我們注意的是在針對一些核心業(yè)務數(shù)據(jù)和非核心業(yè)務數(shù)據(jù)盡量不要共用一個消費者組,防止影響核心數(shù)據(jù)的消費速率。同時我們在做設計的時候多想想這么做有什么優(yōu)點,又有什么缺點,開發(fā)成本問題等。
其實在其他的地方我們可以用到原型模式,比如我們在發(fā)松活動的PUSH通知,針對平臺百萬、千萬、甚至上億的用戶發(fā)送通知的時候通知的內(nèi)容基本都是一樣的只是推送用戶不一樣或者有些特別字段值的小改動,那我們這里就可以用原型模式來做,同時開啟多線程來做push,需要注意的是這里的線程安全問題,所以在每個線程內(nèi)部去做copy對象。
總結(jié)
原型模式使用起來簡單,但是在我們每次在clone基類或者有引用對象的時候需要我們?nèi)バ薷脑蛯ο蟮腸lone方法,這不符合我們開閉原則。
在一般情況下是不建議用這種模式的除非創(chuàng)建的對象成本特別大,或者在一些特殊場景使用,最后針對一些不常用的模式我不會詳細跟大家分享,但是我會在后面做個分享總結(jié),后面開始為大家分享行為型模式。
我是敖丙,你知道的越多,你不知道的越多,我們下期見!