前言
設計模式是軟件工程中一些問題的統一解決方案的模型,它的出現是為了解決一些普遍存在的,卻不能被語言特性直接解決的問題,隨着軟件工程的發展,設計模式也會不斷的進行更新,本文介紹的是經典設計模式-簡單工廠模式以及來自java8的lambda的對它的優化。
什么是簡單工廠模式
概念
定義一個工廠類,對實現了同一接口的一些類進行實例的創建。簡單工廠模式的實質是由一個工廠類根據傳入的參數,動態決定應該創建哪一個產品類(這些產品類繼承自一個父類或接口)的實例
簡單理解
我的理解是工廠模式好比一個容器,里面裝了有許多共同特征的對象,通提供過工廠對外提供的方法向外提供實例化子類的功能,和現實的中的工廠很像。簡明點說,是許多對象的集合,根據需求對外提供不同的對象。
例子
場景描述
在寫了幾個設計模式的博客之后我發現每次都要虛構一個不存在的例子很費腦筋,於是我決定后面的例子用我平常喜歡玩的一些游戲來描述,感覺會更有意思:)
在一片古老的魔法大陸上,有許多隱世的秘寶等待探險者去挖掘,可這樣的機會往往也伴隨着危險,所以探險者們往往需要結伴而行,一般來說,一個不會在野外直接當掉的隊伍至少需要保證三種類型的職業(坦克,輸出,治療,俗稱'鐵三角')。因此,在這樣的需求下,久而久之,魔法大路上誕生了一家'冒險者雇佣兵工廠',沒有人知道這家工廠是何時誕生,也不知道里面究竟有怎樣的實力...只是知道,你給它錢,和你需要的職業,它就會提供一個對應職業的雇佣兵助你完成這次冒險....
有一天,有一個戰士(坦克)阿呆收到消息,有一個叫做'火焰洞窟'里面可能有好東西,可他身邊沒有伙伴一個人顯然是不能去送死的,於是為了快速湊到伙伴,他想到了雇佣兵工廠...他需要一個能夠釋放冰霜法術的法師(輸出)(冰屬性可以克制火焰洞窟里的怪物)和一個能夠療傷的牧師(治療)這兩個職業,下面在客戶端中模擬場景
傳統實現
首頁抽象坦克,輸出,治療為探險者接口,提供一個戰斗的技能的方法
探險者接口
public interface adventurer {
/**
* 使用戰斗技能
*/
void useBattleSkill();
}
戰士,冰霜法師,牧師實現探險者接口,作為子類提供不同的戰斗技能實現
戰士類
public class warrior implements adventurer {
@Override
public void useBattleSkill() {
System.out.println("盾牌格擋!");
}
}
冰霜法師類
public class frostMage implements adventurer {
@Override
public void useBattleSkill() {
System.out.println("寒冰箭!");
}
}
牧師類
public class priests implements adventurer {
@Override
public void useBattleSkill() {
System.out.println("快速治療!");
}
}
冒險者工廠類,根據不同的職業需求實例化不同的冒險者給客戶端
public class adventFactory {
public static adventurer createAdventurer(String professionType) {
adventurer adventurer;
switch (professionType) {
case "戰士":
adventurer = new warrior();
break;
case "冰霜法師":
adventurer = new frostMage();
break;
case "牧師":
adventurer = new priests();
break;
default:
throw new IllegalArgumentException("我們沒這種職業!");
}
return adventurer;
}
}
客戶端類,模擬三個職業進入火焰洞窟並使用各自的技能
public class Client {
public static void main(String[] args) {
//通過冒險者工廠實例化出戰士,冰霜法師,牧師
adventurer warrior = adventFactory.createAdventurer("戰士");
adventurer frostMage = adventFactory.createAdventurer("冰霜法師");
adventurer priest = adventFactory.createAdventurer("牧師");
//進入火焰洞窟
System.out.println("================進入火焰洞窟================");
warrior.useBattleSkill();
frostMage.useBattleSkill();
priest.useBattleSkill();
}
}
控制台結果
================進入火焰洞窟================
盾牌格擋!
寒冰箭!
快速治療!
如同上文所講,雇佣兵工廠通過switch語句根據不同的輸出實例化不同的對象給客戶端調用,這樣客戶端只需要和工廠打交道,有什么需求提供給工廠,工廠實例化出對應對象返回,所以工廠可以理解為是對象實例化的集合。
總結與思考
總結
為了增加趣味性(主要是我自己的..編例子很無聊T_T),本文使用了MMORPG游戲的鐵三角的組隊進副本的例子,冒險者工廠為冒險者提供不同職業的冒險者,冒險者不需要與具體的同伴溝通,通過工廠就可以完成需求,可以說是將需求者與雇佣兵這兩類人給解耦了,通過冒險工廠來交互。從封裝角度來說,之前寫的命令模式,策略模式都是對行為的封裝,而工廠模式是對對象構造器的封裝,這一點也為后面的lambda的優化選擇接口提供了依據。
下面是uml圖
優點
- 解耦,將需求類與實現類分離開了,通過工廠類進行交互
- 無論是添加,修改還是刪除新的子類,都十分的容易,不會影響到其他的類
- 復用,子類可以多次復用,而不是每次都需要復制原先的代碼
可優化點
- 依舊是針對switch語句的優化
- 違背了開閉原則,即增加新的子類之后,原先的工廠類的代碼還需要做改動,開放了修改
優化思路
- 傳統使用反射來完成修改的關閉,這里我不想使用反射來完成,試試lambda能否完成它的職責
使用lambda進行優化
前面提到簡單工廠模式的封裝模式是對對象的構造進行封裝,那么如果采用函數接口替換switch語句的話,選擇的函數應該是Supplier<T>
(無參構造函數) 或者Funtion<T,R>
(有參構造函數),這里我們選擇無參構造函數來進行優化,使用Map存儲這些構造方法,並利用函數語言的懶加載特性,使得直到真正調用實例化對象的某一方法時,才真正調用構造函數,代碼如下。
使用supplier封裝構造器優化后的Factory類
public class adventFactory {
private static final Map<String, Optional<Supplier<adventurer>>> MAP = new ConcurrentHashMap<>();
static {
MAP.put("戰士", Optional.of(warrior::new));
MAP.put("冰霜法師", Optional.of(frostMage::new));
MAP.put("牧師", Optional.of(priests::new));
}
public static adventurer createAdventurer(String professionType) {
//get(professionType)獲得optional對象,orElseThrow用於防止或者異常參數,get()及早求值,執行對象的實例化,直到這一步函數才真正的執行
return MAP.get(professionType)
.orElseThrow(() -> new IllegalArgumentException("我們工廠沒這種職業!"))
.get();
}
}
客戶端代碼與原先一模一樣,這里就不顯示了,下面說明一下這個Factory類。
使用supplier函數接口將構造器封裝,並存儲在MAP中,注意這里與傳統的直接存實例好的對象進去不同,這里存儲的只是構造過程,並不會真正的占用空間,除非客戶端調用create方法需要這個對象了,才會實例化出來,這里利用了函數的懶加載特性。同時為了防止可惡的空指針異常或者是需求並不存在的類,在supplier的基礎上使用了optional類進行包裝,避免了各類if判斷,可以看出使用了lambda優化之后,已經不存在任何的條件判斷語句(switch,if)了,將面向對象與函數語言特性相結合,感覺很不錯。
枚舉的進一步優化
前面提到可優化點的時候提到了簡單工廠方法違背了開閉原則,然而經過lambda優化之后的方式雖然消除了switch與if分支,但是似乎並沒有克服這個問題,工廠類依舊是違背這個原則的,那么可不可能再次優化呢?我認為這種需要傳入魔法值來做一些事情的方法或者設計模式,枚舉都是一個不錯的選擇,下面嘗試使用枚舉。
使用枚舉變量封裝這些構造器,這樣不僅可以使得工廠可以將修改關閉,同時也省去了optional類的包裝,因為你傳入的參數只能是枚舉變量已經定義好的。下面是代碼。
枚舉類,內部存一個supplier對象,存放各大職業的構造器,對外暴露getConstructor方法進行實例化
public enum adventEnum {
WARRIOR(warrior::new),
MAGE_FROST(frostMage::new),
PRIESTS(priests::new);
private final Supplier<adventurer> constructor;
adventEnum(Supplier<adventurer> constructor) {
this.constructor = constructor;
}
public Supplier<adventurer> getConstructor() {
return constructor;
}
}
工廠類
public class adventFactory {
public static adventurer createAdventurer(adventEnum adventEnum) {
adventEnum.getConstructor().get();
}
}
工廠類十分簡潔,然而不僅簡潔,還完美繼承了上面的所有優勢,並且克服了劣勢。
客戶端
import static com.lambda.enums.adventEnum.*;
public class Client {
public static void main(String[] args) {
//通過冒險者工廠實例化出戰士,冰霜法師,牧師
adventurer warrior = adventFactory.createAdventurer(WARRIOR);
adventurer frostMage = adventFactory.createAdventurer(MAGE_FROST);
adventurer priest = adventFactory.createAdventurer(PRIESTS);
//進入火焰洞窟
System.out.println("================進入火焰洞窟================");
warrior.useBattleSkill();
frostMage.useBattleSkill();
priest.useBattleSkill();
}
}
客戶端的調用參數變成了枚舉類,這里靜態導入枚舉類,我一直覺得使用枚舉變量的代碼擁有一種自注釋的特性,即不需寫注釋就可以看的很明了。
結尾
麻雀雖小,五臟俱全,例子很簡單,但是最后的成果是面向對象語言+函數式語言+枚舉
的結合,可以看到這種組合效果是十分棒的,代碼不僅簡潔易用性高同時還保持了健壯性與可擴展性,希望大家可以多嘗試,我認為多種語言范式的組合的語言可能是第三代語言或者更新的語言發展的趨勢吧(Scala,C#等)^_^,大家下篇再見。
關於本文代碼
本文的代碼與md文章同步更新在github中的simple-factory-mode模塊下,歡迎fork 😃