大家好,我是樓仔呀。
無論大家工作還是面試,都會用到設計模式,如果不結合具體的場景,通過書本學到的設計模式非常容易忘。
本文通過具體的示例,教大家如何學習設計模式,保證你看完這篇文章后,這 3 種常用的設計模式,能妥妥掌握!
不 BB,上文章目錄。

1. 一起打豆豆
有個記者去南極采訪一群企鵝,他問第一只企鵝:“你每天都干什么?”
企鵝說:“吃飯,睡覺,打豆豆!”
接著又問第 2 只企鵝,那只企鵝還是說:“吃飯,睡覺,打豆豆!”
記者帶著困惑問其他的企鵝,答案都一樣,就這樣一直問了 99 只企鵝。
當走到第 100 只小企鵝旁邊時,記者走過去問它:每天都做些什么啊?
那只小企鵝回答:"吃飯,睡覺."
記者驚奇的又問:"你怎么不打豆豆?"
小企鵝撇著嘴巴,瞪了記者一眼說:"我就是豆豆!"
樓哥,你搞錯了吧,這是篇技術文,你咋講笑話了?甭著急,繼續往后面看哈~~
2. 模板&策略模式
2.1 實現姿勢
我們會從簡單到復雜,講解代碼正確的實現姿勢,分別為最 Low 方式、常規方式、模板模式和策略模式。
2.1.1 最 Low 方式
假如現在有 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();
}
}
看一下執行結果:
littlePenguin:
吃飯
睡覺
用小翅膀打豆豆
middlePenguin:
吃飯
睡覺
用圓圓的肚子打豆豆
bigPenguin:
吃飯
睡覺
拿雞毛撣子打豆豆
這種方式是大家寫代碼時,最容易使用的方式,上手簡單,也容易理解,目前看項目中陳舊的代碼,經常能找到它們的影子,下面我們看怎么一步步將其進行重構。
2.1.2 常規方式
“吃飯,睡覺,打豆豆” 其實都是獨立的行為,為了不相互影響,我們可以通過函數簡單進行封裝:
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();
// 下同,省略...
}
}
工作過一段時間的同學,可能會采用這種實現方式,我們有沒有更優雅的實現方式呢?
2.1.3 模板模式
定義:一個抽象類公開定義了執行它的方法的方式/模板,它的子類可以按需要重寫方法實現,但調用將以抽象類中定義的方式進行,屬于行為型模式。
這 3 只企鵝,因為 “吃飯,睡覺” 都一樣,所以我們可以直接實現出來,但是他們 “打豆豆” 的方式不同,所以封裝成抽象方法,需要每個企鵝單獨去實現 “打豆豆” 的方式。
最后再新增一個方法 everyDay(),固定每天的執行流程:
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();
}
}
每只企鵝單獨實現自己 “打豆豆” 的方式:
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("拿雞毛撣子打豆豆");
}
}
最后看調用方式:
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();
}
}
樓哥,你這代碼看的費勁,能給我畫一個 UML 圖么?來,安排!

2.1.4 策略模式
定義:一個類的行為或其算法可以在運行時更改,即我們創建表示各種策略的對象和一個行為隨著策略對象改變而改變的 context 對象,策略對象改變 context 對象的執行算法,屬于行為型模式。
我們還是先抽象出 3 個企鵝的行為:
每只企鵝單獨實現自己 “打豆豆” 的方式:
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("拿雞毛撣子打豆豆");
}
}
這里就是策略模式的重點,我們再看一下策略模式的定義 “我們創建表示各種策略的對象和一個行為隨著策略對象改變而改變的 context 對象”,那么該 contex 對象如下:
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();
}
}
最后看調用方式:
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 傳遞不同的對象,然后來約定 everyDay() 的調用方式。
其實我這個示例,有點把策略模式講復雜了,因為純粹的策略模式,3 個企鵝只有 beating() 方法不同,所以可以把 beating() 理解為不同的算法即可。
之所以引入 everyDay(),是因為實際的項目場景中,會經常這么使用,也就是把這個變化的算法 beating(),包裝到具體的執行流程里面,所以策略模式就看起來沒有那么直觀,但是核心思想是一樣的。

2.2 模板 vs 策略
我在選擇模板模式和策略模式時,發現兩者都可以完全滿足我的需求,然后我到網上查閱了很多資料,希望能找到兩種模式在技術選擇時,能確定告訴我哪些情況需要選擇哪種模式。
說來慚愧,到現在我都沒有找到,因為網上只告訴我兩種實現姿勢的區別,但是沒有說明如何具體選型。
2.2.1 網上觀點
據我可以告訴他們是 99% 相同,唯一的區別是模板方法模式具有抽象類作為基類,而戰略類使用由每個具體戰略類實現的接口,兩者的主要區別在于具體 algorithm 的 select。
使用 Template 方法模式時,通過子類化模板在編譯時發生,每個子類通過實現模板的抽象方法提供了一個不同的具體 algorithm。
當客戶端調用模板的外部接口的方法時,模板根據需要調用其抽象方法(其內部接口)來調用 algorithm。
相比之下,策略模式允許在運行時通過遏制來 select algorithm,具體 algorithm 是通過單獨的類或函數實現的,這些類或函數作為 parameter passing 給構造函數或構造方法。
上面講的有點抽象,下面直接看具體對比。

相似:
- 策略和模板方法模式都可以用來滿足開閉原則,使得軟件模塊在不改變代碼的情況下易于擴展;
- 兩種模式都表示通用 function 與該 function 的詳細實現的分離,不過它們所提供的粒度有一些差異。
差異:
它基于接口;
客戶和策略之間的耦合更加松散;
定義不能被子類改變的 algorithm 的骨架,只有某些操作可以在子類中重寫;
父類完全控制 algorithm ,僅將具體的步驟與具體的類進行區分;
綁定是在編譯時完成的。
它基于框架或抽象類,甚至可以有一個具有默認實現的具體類。
模塊耦合得更緊密;
它通過修改方法的行為來改變對象的內容;
它用于在 algorithm 族之間切換;
它在運行時通過其他 algorithm 完全 replace 一個algorithm 來改變對象的行為;
綁定在運行時完成。
2.2.2 個人理解
對于有強迫癥的我,沒有找到問題的根源,總感覺哪里不對勁,我就說一下我對于兩者區別的理解吧。
說實話,兩種設計模式,我也就看到在實現姿勢上有所區別,至于說的策略模式要定義統一接口,模板模式不這樣做等,我不太贊同,因為我有時也會給模板模式定義一個通用接口。
然后也有人說,策略模式需要定義一堆對象,模板模式就不需要,如果有 10 個不同的企鵝,模板模式不也是需要定義 10 個不同的企鵝類,然后再專門針對特定的方法去實現么?
這兩種設計模式,我感覺還沒有到非此即彼的劃分。
如果我沒有固定的執行流程,比如只去打豆豆,只需要對一個方法做具體抽象,我愿意選擇策略模式,因為這個我感覺會讓我需要使用的對象,更清晰一些。
如果我有固定的執行流程,比如 “吃飯、睡覺、打豆豆”,我更愿意使用模板方法,可能是代碼看多了,也看習慣了,更愿意用模板方法去規范代碼固定的執行流程。
當然,我也可以將兩者結合起來使用,比如我們可以用模板方法,去實現這 3 只企鵝,但是對于 middlePenguin,可能有分為企鵝少年 A、企鵝少年 B、企鵝少年 C,他們都喜歡隔壁的企鵝妹妹,但是喜歡的方式不同,有暗戀的,有直接表白的,還有霸道總裁的,我可以用策略模式,去指定他們對企鵝妹妹的表達方式。
2.3 實際場景
任何模式,都需要結合實際的場景來講,才能更清晰。
這兩個模式,可以在你之前做過的項目中,只要稍微留意一下,應該會發現它們其實是大量存在的。
比如很多框架代碼,里面有很多固定的執行流程,有些邏輯是可以采用默認處理的方式,有些邏輯需要下游自己去實現,然后有些邏輯還需要提前預留鉤子。
比如在執行 process() 流程時,可能需要進行 preProcess() 的操作,那么這個 preProcess() 就是你預留的鉤子,下游可以實現,也可以不實現。
3. 工廠模式
后面會結合模板模式,來講解工廠模式,實戰場景非常強。
3.1 問題引入
對于工廠模式,大家可能覺得會很 Low,不就是搞個類,然后專門生成一個具體的對象嘛,這有什么難的?
是的,工廠模式確實不難,但是問你一下,如果你的代碼中有很多 if...else,你知道怎么通過工廠模式,把這些 if...else 去掉么?
嗯,工廠模式我會,但是和去掉 if...else 好像沒有關系吧?
我舉個例子,假如你遇到如下代碼:
switch($taskInfo['type_id']) {
//批量凍結訂單
case 1:
$result = self::batchFrozen($row_key,1);
break;
//批量解凍訂單
case 2:
$result = self::batchFrozen($row_key,0);
break;
//批量允許發貨
case 3:
$result =self::batchReshipment($row_key);
break;
//批量取消發貨
case 4:
$result = self::batchCancel($row_key);
break;
// 后面還有幾十個case,省略...
既然你懂工廠模式,可以把 if...else 簡單重構一下,那就開始你的表演吧。
什么?不會?!你剛才還是自己是會工廠模式,怎么突然就慫了呢?
既然不會,那就靜下心來,虛心學習一下。
3.2 工廠模式
定義:它提供了一種創建對象的最佳方式,我們在創建對象時不會對客戶端暴露創建邏輯,并且是通過使用一個共同的接口來指向新創建的對象,屬于創建型模式。
先直接上圖,后面的示例主要通過該圖展開:

其實設計模式一般不會單一使用,通常會和其它模式結合起來使用,這里我們就將上一篇文章講到的模板模式和工廠模式結合起來。
因為工廠模式,通常會給這些新創建的對象制定一個公共的接口,我們可以通過抽象類定義:
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();
}
}
因為我們是結合了模板模式,所以這個抽象類中,可以看到模板模式的影子。
如果你只關注抽象的接口,比如 beating,那么這個就是一個抽象方法,也可以理解為下游需要實現的方法,其它的接口其實可以忽略。
看看每個企鵝具體的實現:
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("拿雞毛撣子打豆豆");
}
}
這里是工廠方法的重點,需要構建一個工廠,專門用來拿企鵝:
public class penguinFactory {
private static final Map<String, penguin> 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);
}
}
上面的邏輯很簡單,就是通過一個 map 對象,放入所有的企鵝,這個工廠就可以通過企鵝的名字,拿到對應的企鵝對象,最后我們看使用方式:
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();
}
}
輸出如下:
吃飯
睡覺
用小翅膀打豆豆
吃飯
睡覺
用圓圓的肚子打豆豆
吃飯
睡覺
拿雞毛撣子打豆豆
樓哥,你這個例子我看懂了,但是你最開始拋的那個問題,能給出答案么?
3.3 問題解答
文章開頭的這個示例,其實也是我最近需要重構項目中的一段代碼,我就是用 “工廠模式 + 模板模式” 來重構的。
我首先會對每個方法中的內容通過模板模式進行抽象(因為本章主要講工廠模式,模板模式的代碼,我就不貼了),然后通過工廠模式獲取不同的對象,直接看重構后的代碼:
public class TaskFactory {
@Autowired
public static List<AbstractTask> taskList;
private static final Map<String, AbstractTask> map = new HashMap<>();
static {
// 存放任務映射關系
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<CancelParams> params = new ParamWrapper<CancelParams>();
params.rowKey = 11111111;
params.data = new CancelParams();
OcApiServerResponse res = task.execute(params);
System.out.println(res.toString());
return;
}
}
3.4 實際場景
這個場景就太多了,剛才給大家講解的是去掉 if...else 的場景,然后在小米商城的支付系統中,因為海外有幾十種支付方式,也是通過這種方式去掉 if...else 的,不過支付類的封裝不是用的模板方法,用的的策略模式,雖然感覺兩者差不多。
如果你直接 new 一個對象就能解決的問題,就用不到工廠模式了。
4. 結語
看完這篇文章,相信這 3 種設計模式,已經深深刻在你骨子里面了。
大家可以靜下心來想想,自己之前做過的項目中,有哪些用到上面這 3 種設計模式,然后自己再結合具體的場景總結一下,我想你應該會有更深入的理解。