掃二維碼與項(xiàng)目經(jīng)理溝通
我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
大家好,我是樓仔呀。

圖木舒克網(wǎng)站建設(shè)公司成都創(chuàng)新互聯(lián)公司,圖木舒克網(wǎng)站設(shè)計(jì)制作,有大型網(wǎng)站制作公司豐富經(jīng)驗(yàn)。已為圖木舒克上千多家提供企業(yè)網(wǎng)站建設(shè)服務(wù)。企業(yè)網(wǎng)站搭建\外貿(mào)網(wǎng)站建設(shè)要多少錢,請(qǐng)找那個(gè)售后服務(wù)好的圖木舒克做網(wǎng)站的公司定做!
無論大家工作還是面試,都會(huì)用到設(shè)計(jì)模式,如果不結(jié)合具體的場(chǎng)景,通過書本學(xué)到的設(shè)計(jì)模式非常容易忘。
本文通過具體的示例,教大家如何學(xué)習(xí)設(shè)計(jì)模式,保證你看完這篇文章后,這 3 種常用的設(shè)計(jì)模式,能妥妥掌握!
不 BB,上文章目錄。
有個(gè)記者去南極采訪一群企鵝,他問第一只企鵝:“你每天都干什么?”
企鵝說:“吃飯,睡覺,打豆豆!”
接著又問第 2 只企鵝,那只企鵝還是說:“吃飯,睡覺,打豆豆!”
記者帶著困惑問其他的企鵝,答案都一樣,就這樣一直問了 99 只企鵝。
當(dāng)走到第 100 只小企鵝旁邊時(shí),記者走過去問它:每天都做些什么啊?
那只小企鵝回答:"吃飯,睡覺."
記者驚奇的又問:"你怎么不打豆豆?"
小企鵝撇著嘴巴,瞪了記者一眼說:"我就是豆豆!"
樓哥,你搞錯(cuò)了吧,這是篇技術(shù)文,你咋講笑話了?甭著急,繼續(xù)往后面看哈~~
我們會(huì)從簡(jiǎn)單到復(fù)雜,講解代碼正確的實(shí)現(xiàn)姿勢(shì),分別為最 Low 方式、常規(guī)方式、模板模式和策略模式。
2.1.1 最 Low 方式
假如現(xiàn)在有 3 只企鵝,都喜歡 “吃飯,睡覺,打豆豆”:
public class littlePenguin {
public void everyDay(){
System.out.println("吃飯");
System.out.println("睡覺");
System.out.println("用小翅膀打豆豆");
}
}
public class middlePenguin {
public void everyDay(){
System.out.println("吃飯");
System.out.println("睡覺");
System.out.println("用圓圓的肚子打豆豆");
}
}
public class bigPenguin {
public void everyDay(){
System.out.println("吃飯");
System.out.println("睡覺");
System.out.println("拿雞毛撣子打豆豆");
}
}
public class test {
public static void main(String[] args){
System.out.println("littlePenguin:");
littlePenguin penguin_1 = new littlePenguin();
penguin_1.everyDay();
System.out.println("middlePenguin:");
middlePenguin penguin_2 = new middlePenguin();
penguin_2.everyDay();
System.out.println("bigPenguin:");
bigPenguin penguin_3 = new bigPenguin();
penguin_3.everyDay();
}
}看一下執(zhí)行結(jié)果:
littlePenguin:
吃飯
睡覺
用小翅膀打豆豆
middlePenguin:
吃飯
睡覺
用圓圓的肚子打豆豆
bigPenguin:
吃飯
睡覺
拿雞毛撣子打豆豆
這種方式是大家寫代碼時(shí),最容易使用的方式,上手簡(jiǎn)單,也容易理解,目前看項(xiàng)目中陳舊的代碼,經(jīng)常能找到它們的影子,下面我們看怎么一步步將其進(jìn)行重構(gòu)。
2.1.2 常規(guī)方式
“吃飯,睡覺,打豆豆” 其實(shí)都是獨(dú)立的行為,為了不相互影響,我們可以通過函數(shù)簡(jiǎn)單進(jìn)行封裝:
public class littlePenguin {
public void eating(){
System.out.println("吃飯");
}
public void sleeping(){
System.out.println("睡覺");
}
public void beating(){
System.out.println("用小翅膀打豆豆");
}
}
public class middlePenguin {
public void eating(){
System.out.println("吃飯");
}
public void sleeping(){
System.out.println("睡覺");
}
public void beating(){
System.out.println("用圓圓的肚子打豆豆");
}
}
// bigPenguin相同,省略...
public class test {
public static void main(String[] args){
System.out.println("littlePenguin:");
littlePenguin penguin_1 = new littlePenguin();
penguin_1.eating();
penguin_1.sleeping();
penguin_1.beating();
// 下同,省略...
}
}工作過一段時(shí)間的同學(xué),可能會(huì)采用這種實(shí)現(xiàn)方式,我們有沒有更優(yōu)雅的實(shí)現(xiàn)方式呢?
2.1.3 模板模式
定義:一個(gè)抽象類公開定義了執(zhí)行它的方法的方式/模板,它的子類可以按需要重寫方法實(shí)現(xiàn),但調(diào)用將以抽象類中定義的方式進(jìn)行,屬于行為型模式。
這 3 只企鵝,因?yàn)?“吃飯,睡覺” 都一樣,所以我們可以直接實(shí)現(xiàn)出來,但是他們 “打豆豆” 的方式不同,所以封裝成抽象方法,需要每個(gè)企鵝單獨(dú)去實(shí)現(xiàn) “打豆豆” 的方式。
最后再新增一個(gè)方法 everyDay(),固定每天的執(zhí)行流程:
public abstract class penguin {
public void eating(){
System.out.println("吃飯");
}
public void sleeping(){
System.out.println("睡覺");
}
public abstract void beating();
public void everyDay(){
this.eating();
this.sleeping();
this.beating();
}
}每只企鵝單獨(dú)實(shí)現(xiàn)自己 “打豆豆” 的方式:
public class littlePenguin extends penguin {
@Override
public void beating(){
System.out.println("用小翅膀打豆豆");
}
}
public class middlePenguin extends penguin {
@Override
public void beating(){
System.out.println("用圓圓的肚子打豆豆");
}
}
public class bigPenguin extends penguin {
@Override
public void beating(){
System.out.println("拿雞毛撣子打豆豆");
}
}最后看調(diào)用方式:
public class test {
public static void main(String[] args){
System.out.println("littlePenguin:");
littlePenguin penguin1 = new littlePenguin();
penguin1.everyDay();
System.out.println("middlePenguin:");
middlePenguin penguin2 = new middlePenguin();
penguin2.everyDay();
System.out.println("bigPenguin:");
bigPenguin penguin3 = new bigPenguin();
penguin3.everyDay();
}
}樓哥,你這代碼看的費(fèi)勁,能給我畫一個(gè) UML 圖么?來,安排!
2.1.4 策略模式
定義:一個(gè)類的行為或其算法可以在運(yùn)行時(shí)更改,即我們創(chuàng)建表示各種策略的對(duì)象和一個(gè)行為隨著策略對(duì)象改變而改變的 context 對(duì)象,策略對(duì)象改變 context 對(duì)象的執(zhí)行算法,屬于行為型模式。
我們還是先抽象出 3 個(gè)企鵝的行為:
每只企鵝單獨(dú)實(shí)現(xiàn)自己 “打豆豆” 的方式:
public class littlePenguin extends penguin {
@Override
public void beating(){
System.out.println("用小翅膀打豆豆");
}
}
public class middlePenguin extends penguin {
@Override
public void beating(){
System.out.println("用圓圓的肚子打豆豆");
}
}
public class bigPenguin extends penguin {
@Override
public void beating(){
System.out.println("拿雞毛撣子打豆豆");
}
}這里就是策略模式的重點(diǎn),我們?cè)倏匆幌虏呗阅J降亩x “我們創(chuàng)建表示各種策略的對(duì)象和一個(gè)行為隨著策略對(duì)象改變而改變的 context 對(duì)象”,那么該 contex 對(duì)象如下:
public class behaviorContext {
private penguin _penguin;
public behaviorContext(penguin newPenguin){
_penguin = newPenguin;
}
public void setPenguin(penguin newPenguin){
_penguin = newPenguin;
}
public void everyDay(){
_penguin.eating();
_penguin.sleeping();
_penguin.beating();
}
}最后看調(diào)用方式:
public class test {
public static void main(String[] args){
behaviorContext behavior = new behaviorContext(new littlePenguin());
behavior.everyDay();
behavior.setPenguin(new middlePenguin());
behavior.everyDay();
behavior.setPenguin(new bigPenguin());
behavior.everyDay();
}
}我們可以通過給 behaviorContext 傳遞不同的對(duì)象,然后來約定 everyDay() 的調(diào)用方式。
其實(shí)我這個(gè)示例,有點(diǎn)把策略模式講復(fù)雜了,因?yàn)榧兇獾牟呗阅J剑? 個(gè)企鵝只有 beating() 方法不同,所以可以把 beating() 理解為不同的算法即可。
之所以引入 everyDay(),是因?yàn)閷?shí)際的項(xiàng)目場(chǎng)景中,會(huì)經(jīng)常這么使用,也就是把這個(gè)變化的算法 beating(),包裝到具體的執(zhí)行流程里面,所以策略模式就看起來沒有那么直觀,但是核心思想是一樣的。
我在選擇模板模式和策略模式時(shí),發(fā)現(xiàn)兩者都可以完全滿足我的需求,然后我到網(wǎng)上查閱了很多資料,希望能找到兩種模式在技術(shù)選擇時(shí),能確定告訴我哪些情況需要選擇哪種模式。
說來慚愧,到現(xiàn)在我都沒有找到,因?yàn)榫W(wǎng)上只告訴我兩種實(shí)現(xiàn)姿勢(shì)的區(qū)別,但是沒有說明如何具體選型。
2.2.1 網(wǎng)上觀點(diǎn)
據(jù)我可以告訴他們是 99% 相同,唯一的區(qū)別是模板方法模式具有抽象類作為基類,而戰(zhàn)略類使用由每個(gè)具體戰(zhàn)略類實(shí)現(xiàn)的接口,兩者的主要區(qū)別在于具體 algorithm 的 select。
使用 Template 方法模式時(shí),通過子類化模板在編譯時(shí)發(fā)生,每個(gè)子類通過實(shí)現(xiàn)模板的抽象方法提供了一個(gè)不同的具體 algorithm。
當(dāng)客戶端調(diào)用模板的外部接口的方法時(shí),模板根據(jù)需要調(diào)用其抽象方法(其內(nèi)部接口)來調(diào)用 algorithm。
相比之下,策略模式允許在運(yùn)行時(shí)通過遏制來 select algorithm,具體 algorithm 是通過單獨(dú)的類或函數(shù)實(shí)現(xiàn)的,這些類或函數(shù)作為 parameter passing 給構(gòu)造函數(shù)或構(gòu)造方法。
上面講的有點(diǎn)抽象,下面直接看具體對(duì)比。
相似:
差異:
它基于接口;
客戶和策略之間的耦合更加松散;
定義不能被子類改變的 algorithm 的骨架,只有某些操作可以在子類中重寫;
父類完全控制 algorithm ,僅將具體的步驟與具體的類進(jìn)行區(qū)分;
綁定是在編譯時(shí)完成的。
它基于框架或抽象類,甚至可以有一個(gè)具有默認(rèn)實(shí)現(xiàn)的具體類。
模塊耦合得更緊密;
它通過修改方法的行為來改變對(duì)象的內(nèi)容;
它用于在 algorithm 族之間切換;
它在運(yùn)行時(shí)通過其他 algorithm 完全 replace 一個(gè)algorithm 來改變對(duì)象的行為;
綁定在運(yùn)行時(shí)完成。
2.2.2 個(gè)人理解
對(duì)于有強(qiáng)迫癥的我,沒有找到問題的根源,總感覺哪里不對(duì)勁,我就說一下我對(duì)于兩者區(qū)別的理解吧。
說實(shí)話,兩種設(shè)計(jì)模式,我也就看到在實(shí)現(xiàn)姿勢(shì)上有所區(qū)別,至于說的策略模式要定義統(tǒng)一接口,模板模式不這樣做等,我不太贊同,因?yàn)槲矣袝r(shí)也會(huì)給模板模式定義一個(gè)通用接口。
然后也有人說,策略模式需要定義一堆對(duì)象,模板模式就不需要,如果有 10 個(gè)不同的企鵝,模板模式不也是需要定義 10 個(gè)不同的企鵝類,然后再專門針對(duì)特定的方法去實(shí)現(xiàn)么?
這兩種設(shè)計(jì)模式,我感覺還沒有到非此即彼的劃分。
如果我沒有固定的執(zhí)行流程,比如只去打豆豆,只需要對(duì)一個(gè)方法做具體抽象,我愿意選擇策略模式,因?yàn)檫@個(gè)我感覺會(huì)讓我需要使用的對(duì)象,更清晰一些。
如果我有固定的執(zhí)行流程,比如 “吃飯、睡覺、打豆豆”,我更愿意使用模板方法,可能是代碼看多了,也看習(xí)慣了,更愿意用模板方法去規(guī)范代碼固定的執(zhí)行流程。
當(dāng)然,我也可以將兩者結(jié)合起來使用,比如我們可以用模板方法,去實(shí)現(xiàn)這 3 只企鵝,但是對(duì)于 middlePenguin,可能有分為企鵝少年 A、企鵝少年 B、企鵝少年 C,他們都喜歡隔壁的企鵝妹妹,但是喜歡的方式不同,有暗戀的,有直接表白的,還有霸道總裁的,我可以用策略模式,去指定他們對(duì)企鵝妹妹的表達(dá)方式。
任何模式,都需要結(jié)合實(shí)際的場(chǎng)景來講,才能更清晰。
這兩個(gè)模式,可以在你之前做過的項(xiàng)目中,只要稍微留意一下,應(yīng)該會(huì)發(fā)現(xiàn)它們其實(shí)是大量存在的。
比如很多框架代碼,里面有很多固定的執(zhí)行流程,有些邏輯是可以采用默認(rèn)處理的方式,有些邏輯需要下游自己去實(shí)現(xiàn),然后有些邏輯還需要提前預(yù)留鉤子。
比如在執(zhí)行 process() 流程時(shí),可能需要進(jìn)行 preProcess() 的操作,那么這個(gè) preProcess() 就是你預(yù)留的鉤子,下游可以實(shí)現(xiàn),也可以不實(shí)現(xiàn)。
后面會(huì)結(jié)合模板模式,來講解工廠模式,實(shí)戰(zhàn)場(chǎng)景非常強(qiáng)。
對(duì)于工廠模式,大家可能覺得會(huì)很 Low,不就是搞個(gè)類,然后專門生成一個(gè)具體的對(duì)象嘛,這有什么難的?
是的,工廠模式確實(shí)不難,但是問你一下,如果你的代碼中有很多 if...else,你知道怎么通過工廠模式,把這些 if...else 去掉么?
嗯,工廠模式我會(huì),但是和去掉 if...else 好像沒有關(guān)系吧?
我舉個(gè)例子,假如你遇到如下代碼:
switch($taskInfo['type_id']) {
//批量?jī)鼋Y(jié)訂單
case 1:
$result = self::batchFrozen($row_key,1);
break;
//批量解凍訂單
case 2:
$result = self::batchFrozen($row_key,0);
break;
//批量允許發(fā)貨
case 3:
$result =self::batchReshipment($row_key);
break;
//批量取消發(fā)貨
case 4:
$result = self::batchCancel($row_key);
break;
// 后面還有幾十個(gè)case,省略...既然你懂工廠模式,可以把 if...else 簡(jiǎn)單重構(gòu)一下,那就開始你的表演吧。
什么?不會(huì)?!你剛才還是自己是會(huì)工廠模式,怎么突然就慫了呢?
既然不會(huì),那就靜下心來,虛心學(xué)習(xí)一下。
定義:它提供了一種創(chuàng)建對(duì)象的最佳方式,我們?cè)趧?chuàng)建對(duì)象時(shí)不會(huì)對(duì)客戶端暴露創(chuàng)建邏輯,并且是通過使用一個(gè)共同的接口來指向新創(chuàng)建的對(duì)象,屬于創(chuàng)建型模式。
先直接上圖,后面的示例主要通過該圖展開:
其實(shí)設(shè)計(jì)模式一般不會(huì)單一使用,通常會(huì)和其它模式結(jié)合起來使用,這里我們就將上一篇文章講到的模板模式和工廠模式結(jié)合起來。
因?yàn)楣S模式,通常會(huì)給這些新創(chuàng)建的對(duì)象制定一個(gè)公共的接口,我們可以通過抽象類定義:
public abstract class penguin {
public void eating(){
System.out.println("吃飯");
}
public void sleeping(){
System.out.println("睡覺");
}
public abstract void beating();
public void everyDay(){
this.eating();
this.sleeping();
this.beating();
}
}因?yàn)槲覀兪墙Y(jié)合了模板模式,所以這個(gè)抽象類中,可以看到模板模式的影子。
如果你只關(guān)注抽象的接口,比如 beating,那么這個(gè)就是一個(gè)抽象方法,也可以理解為下游需要實(shí)現(xiàn)的方法,其它的接口其實(shí)可以忽略。
看看每個(gè)企鵝具體的實(shí)現(xiàn):
public class littlePenguin extends penguin {
@Override
public void beating(){
System.out.println("用小翅膀打豆豆");
}
}
public class middlePenguin extends penguin {
@Override
public void beating(){
System.out.println("用圓圓的肚子打豆豆");
}
}
public class bigPenguin extends penguin {
@Override
public void beating(){
System.out.println("拿雞毛撣子打豆豆");
}
}這里是工廠方法的重點(diǎn),需要構(gòu)建一個(gè)工廠,專門用來拿企鵝:
public class penguinFactory {
private static final Map map = new HashMap<>();
static {
map.put("littlePenguin", new littlePenguin());
map.put("middlePenguin", new middlePenguin());
map.put("bigPenguin", new bigPenguin());
}
// 獲取企鵝
public static penguin getPenguin(String name){
return map.get(name);
}
} 上面的邏輯很簡(jiǎn)單,就是通過一個(gè) map 對(duì)象,放入所有的企鵝,這個(gè)工廠就可以通過企鵝的名字,拿到對(duì)應(yīng)的企鵝對(duì)象,最后我們看使用方式:
public class test {
public static void main(String[] args){
penguin penguin_1 = penguinFactory.getPenguin("littlePenguin");
penguin_1.everyDay();
penguin penguin_2 = penguinFactory.getPenguin("middlePenguin");
penguin_2.everyDay();
penguin penguin_3 = penguinFactory.getPenguin("bigPenguin");
penguin_3.everyDay();
}
}輸出如下:
吃飯
睡覺
用小翅膀打豆豆
吃飯
睡覺
用圓圓的肚子打豆豆
吃飯
睡覺
拿雞毛撣子打豆豆
樓哥,你這個(gè)例子我看懂了,但是你最開始拋的那個(gè)問題,能給出答案么?
文章開頭的這個(gè)示例,其實(shí)也是我最近需要重構(gòu)項(xiàng)目中的一段代碼,我就是用 “工廠模式 + 模板模式” 來重構(gòu)的。
我首先會(huì)對(duì)每個(gè)方法中的內(nèi)容通過模板模式進(jìn)行抽象(因?yàn)楸菊轮饕v工廠模式,模板模式的代碼,我就不貼了),然后通過工廠模式獲取不同的對(duì)象,直接看重構(gòu)后的代碼:
public class TaskFactory {
@Autowired
public static List taskList;
private static final Map map = new HashMap<>();
static {
// 存放任務(wù)映射關(guān)系
map.put(AbstractTask.OPERATOR_TYPE_FROZEN, new BatchFrozenTask());
map.put(AbstractTask.OPERATOR_TYPE_REJECT, new BatchRejectTask());
map.put(AbstractTask.OPERATOR_TYPE_CANCEL, new BatchCancelTask());
}
public static void main(String[] args){
String operatorType = AbstractTask.OPERATOR_TYPE_REJECT;
AbstractTask task = TaskFactory.map.get(operatorType);
ParamWrapper params = new ParamWrapper();
params.rowKey = 11111111;
params.data = new CancelParams();
OcApiServerResponse res = task.execute(params);
System.out.println(res.toString());
return;
}
} 這個(gè)場(chǎng)景就太多了,剛才給大家講解的是去掉 if...else 的場(chǎng)景,然后在小米商城的支付系統(tǒng)中,因?yàn)楹M庥袔资N支付方式,也是通過這種方式去掉 if...else 的,不過支付類的封裝不是用的模板方法,用的的策略模式,雖然感覺兩者差不多。
如果你直接 new 一個(gè)對(duì)象就能解決的問題,就用不到工廠模式了。
看完這篇文章,相信這 3 種設(shè)計(jì)模式,已經(jīng)深深刻在你骨子里面了。
大家可以靜下心來想想,自己之前做過的項(xiàng)目中,有哪些用到上面這 3 種設(shè)計(jì)模式,然后自己再結(jié)合具體的場(chǎng)景總結(jié)一下,我想你應(yīng)該會(huì)有更深入的理解。

我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流