在日常工作里,需求變動或者新增功能是再常見不過的事情了。而面對這種情況時,那些耦合度較高的代碼就會給我們帶來不少麻煩,因為在這樣的代碼基礎上添加新需求往往困難重重。為了保證系統的穩定性,我們在添加新需求時,最好避免直接修改前人編寫的代碼,否則可能會破壞原有的穩定結構。
接下來,我會為大家介紹兩種非常實用的設計模式 —— 工廠模式和策略模式。如果您已經對這兩種設計模式了如指掌,那么可以直接跳過下面的介紹內容。
下文中出現的案例代碼在:https://gitee.com/dingchen0000/blog-notes.git
工廠模式
介紹
在傳統的編程方式里,當我們需要使用某個對象時,就會直接使用new
關鍵字去創建它,就好像我們自己親手打造一個工具,打造完成后就能馬上使用。但這種方式在項目規模變大、邏輯變復雜后,會帶來很多問題,而工廠模式的出現就是為了解決這些問題。
想象一下,我們在一個電子工廠里工作。你創造出了一個智能機器人,它有靈活的手臂(屬性)和穩健的輪子(屬性),能夠高效地替你組裝零件(方法)。在傳統模式下,你創造出這個機器人后,就可以直接給它下達指令,讓它開始工作。同樣,你的同事創造了一臺智能冰箱,它有超大的存儲空間(屬性)和智能的溫度調節功能(方法),你的同事也能直接操作這臺冰箱。
然而,如果有其他部門的同事想要使用你們創造的機器人和冰箱,就會變得很麻煩。他們需要分別找你和你的同事,經過你們的同意才能使用,這無疑增加了溝通成本和使用的復雜性。
為了解決這個問題,工廠引入了一個統一的管理部門(工廠模式)。你和你的同事把創造好的機器人和冰箱都交給這個管理部門,當其他部門的同事需要使用機器人或冰箱時,只需要向這個管理部門提出申請,管理部門就會根據需求提供相應的設備。這樣一來,使用者不需要關心設備是如何制造出來的,也不需要和具體的創造者溝通,大大提高了使用效率,降低了各個部門之間的耦合度。
在代碼的世界里也是一樣。假設我們有不同的促銷策略,比如打折、滿減、贈品等。如果沒有工廠模式,在需要使用這些策略時,我們就得在代碼里到處使用new
關鍵字來創建策略對象,這樣會讓代碼變得混亂,而且一旦策略的創建邏輯發生變化,就需要修改大量的代碼。而使用工廠模式,我們可以把這些策略對象的創建邏輯封裝在一個工廠類里,當需要使用某個策略時,只需要向工廠類請求,由工廠類來創建并返回相應的對象,這樣就實現了對象的創建和使用的分離,降低了代碼的耦合度,提高了代碼的可維護性和可擴展性。
非工廠案例
在傳統編程中,當需要實現打折促銷功能時,通常會在業務邏輯代碼里直接創建并使用打折策略對象。下面結合你提供的代碼片段,以 Java 為例說明傳統做法:
傳統的模式
// 1. 定義促銷策略接口
interface PromotionStrategy {double calculateDiscount(double orderAmount);
}// 2. 實現具體策略類
class DiscountStrategy implements PromotionStrategy {@Overridepublic double calculateDiscount(double orderAmount) {return orderAmount * 0.2; // 8折優惠}
}class FullReduceStrategy implements PromotionStrategy {@Overridepublic double calculateDiscount(double orderAmount) {return orderAmount >= 200 ? 50 : 0; // 滿200減50}
}// 3. 業務邏輯直接依賴具體策略
class OrderServiceWithoutFactory {public double calculateFinalPrice(double amount, String strategyType) {PromotionStrategy strategy;// 直接在業務邏輯中創建對象if ("DISCOUNT".equalsIgnoreCase(strategyType)) {strategy = new DiscountStrategy();} else if ("FULL_REDUCE".equalsIgnoreCase(strategyType)) {strategy = new FullReduceStrategy();} else {throw new IllegalArgumentException("未知策略類型");}return amount - strategy.calculateDiscount(amount);}
}// 4. 客戶端調用
public class NonFactoryDemo {public static void main(String[] args) {OrderServiceWithoutFactory service = new OrderServiceWithoutFactory();double finalPrice = service.calculateFinalPrice(300, "DISCOUNT");System.out.println("最終價格: " + finalPrice);}
}
-
在非工廠模式中,對象的創建邏輯直接嵌入在業務代碼里,這會導致以下幾個嚴重的維護問題:
代碼分散問題
在復雜系統中,對象創建可能散落在多個服務類、工具類甚至控制器中。例如:
- 訂單服務中直接
new DiscountStrategy()
- 營銷活動模塊中
new FullReduceStrategy()
- 定時任務里也可能創建相同對象
當需要修改或擴展功能時,你需要:
- 找出所有創建該對象的地方
- 逐一修改,可能遺漏某些角落
- 承擔引入新問題的風險
- 訂單服務中直接
缺點:
依賴關系復雜
業務類不僅依賴抽象接口,還依賴具體實現類:
public class OrderServiceWithoutFactory {public double calculatePrice(double amount) {// 直接依賴具體類!PromotionStrategy strategy = new DiscountStrategy(); return strategy.calculate(amount);}
}
這種強依賴導致:
- 新增策略時必須修改業務類代碼
- 策略類的構造函數變化(如增加參數)會影響所有調用處
- 難以進行單元測試(需實例化真實對象而非模擬對象)
在非工廠模式里,直接在業務代碼中「硬編碼」創建對象,就像把鑰匙藏在家里各個抽屜里 —— 看起來方便,實際用的時候全是麻煩:
代碼像撒豆子,改一處得翻遍全項目
想象你要給超市所有收銀臺的「打折功能」升級:
- 原本「滿減策略」的代碼,可能藏在:
- 收銀臺的結賬程序里(
new FullReduceStrategy()
) - 會員系統的積分兌換模塊里(又一個
new FullReduceStrategy()
) - 甚至后臺定時計算報表的腳本里(再來一個
new FullReduceStrategy()
)
- 收銀臺的結賬程序里(
當你想修改滿減規則時:
- 得像偵探一樣,把全項目里所有寫著
FullReduceStrategy
的地方都找出來(可能漏找某個角落) - 每個地方都要改一遍代碼(比如把「滿 200 減 50」改成「滿 300 減 80」)
- 改完還得擔心:有沒有漏掉某個地方?改完其他功能會不會出錯?
業務代碼和具體實現「鎖死」,牽一發而動全身
舉個生活例子:
你開了家奶茶店,菜單上寫著「招牌奶茶 = 紅茶 + 奶精 + 珍珠」(業務邏輯),但你直接在菜單里寫死了「用 A 牌紅茶、B 牌奶精」(依賴具體實現類)。
問題來了:
- 新增口味麻煩:想推出「綠茶版奶茶」,必須把菜單上所有「紅茶」字樣都改成「綠茶」(新增策略必須改業務代碼)。
- 供應商換原料就崩潰:如果 A 牌紅茶停產,你得把菜單上所有「A 牌紅茶」換成「C 牌紅茶」(策略類構造函數修改,所有調用處都得改)。
- 沒法模擬測試:想試試「用椰奶代替奶精」的效果,必須真的買椰奶回來試(單元測試時必須創建真實對象,沒法用模擬數據)。
用代碼舉例就是:
// 業務代碼直接「點名」要某個具體實現類
public class 收銀臺 {public double 計算價格(double 金額) {// 直接「new」一個具體的「滿減策略」,就像直接說「我要A牌紅茶」優惠策略 策略 = new 滿減策略(); return 金額 - 策略.計算優惠(金額);}
}
這樣寫死的后果就是:
- 想換「打折策略」?必須改這里的
new 滿減策略()
為new 打折策略()
。 - 滿減策略的構造函數需要傳參(比如
new 滿減策略(200, 50)
)?所有用到它的地方都得跟著改參數。
工廠模式案例
// 1. 定義促銷策略接口(與非工廠模式相同)
interface PromotionStrategy {double calculateDiscount(double orderAmount);
}// 2. 實現具體策略類(與非工廠模式相同)
class DiscountStrategy implements PromotionStrategy {@Overridepublic double calculateDiscount(double orderAmount) {return orderAmount * 0.2; // 8折優惠}
}class FullReduceStrategy implements PromotionStrategy {@Overridepublic double calculateDiscount(double orderAmount) {return orderAmount >= 200 ? 50 : 0; // 滿200減50}
}// 3. 創建工廠類
class PromotionStrategyFactory {public static PromotionStrategy createStrategy(String strategyType) {if ("DISCOUNT".equalsIgnoreCase(strategyType)) {return new DiscountStrategy();} else if ("FULL_REDUCE".equalsIgnoreCase(strategyType)) {return new FullReduceStrategy();} else {throw new IllegalArgumentException("未知策略類型");}}
}// 4. 業務邏輯通過工廠獲取策略
class OrderServiceWithFactory {public double calculateFinalPrice(double amount, String strategyType) {// 通過工廠獲取策略,不直接依賴具體類PromotionStrategy strategy = PromotionStrategyFactory.createStrategy(strategyType);return amount - strategy.calculateDiscount(amount);}
}// 5. 客戶端調用
public class FactoryDemo {public static void main(String[] args) {OrderServiceWithFactory service = new OrderServiceWithFactory();double finalPrice = service.calculateFinalPrice(300, "FULL_REDUCE");System.out.println("最終價格: " + finalPrice);}
}
工廠模式的好處顯然就是
- 單一修改點:新增策略只需在工廠類中注冊,無需修改業務代碼
- 依賴倒置:業務類只依賴工廠和抽象接口,不依賴具體實現
- 代碼復用:復雜的初始化邏輯只需在工廠中實現一次
- 統一管理:對象創建規則集中維護,便于新增功能和團隊協作
- 可測試性:可以輕松替換工廠實現(如使用模擬工廠)進行單元測試
總結
非工廠模式就像把「建房子的圖紙」和「搬磚的步驟」混在一起寫:
- 簡單場景下看似省事,但項目變大后,代碼會像亂成一團的毛線 ——
- 改一個功能要挖地三尺找代碼
- 牽一發而動全身,改完一處崩十處
- 想測試新功能,必須把真實對象全跑一遍
而工廠模式就像找了個「專業包工頭」(工廠類)專門管搬磚,業務代碼只需要告訴包工頭「我要蓋客廳還是臥室」,剩下的細節全由包工頭處理 —— 既干凈又省心。
策略模式
介紹
策略模式是一種行為型設計模式,其核心思想是:
- 封裝算法族:將不同的算法(或策略)封裝成獨立的類,使它們可以相互替換。
- 解耦算法與使用:讓算法的變化獨立于使用算法的客戶端,從而提高代碼的靈活性和可擴展性
打個比方
你去餐廳吃飯,菜單上有「糖醋排骨」「魚香肉絲」「麻婆豆腐」等菜品(這就是不同的「策略」)。
- 你不需要自己進廚房炒菜(不用關心具體怎么做菜),只需要告訴服務員「我要哪道菜」(調用策略)。
- 服務員(相當于「上下文類」)會根據你的選擇,通知廚房做對應的菜(切換策略)。
策略模式的核心就像這個過程:把不同的「做事方法」封裝起來,需要時隨時切換,而調用者不用知道具體怎么實現。
案例說明
先定義「優惠規則」的統一標準(策略接口)
就像餐廳菜單上寫著「所有菜品都要能算出價格」,我們先定一個接口:
public interface PromotionStrategy {double calculateDiscount(double orderAmount); // 不管怎么優惠,都要能算出優惠金額
}
作用:讓所有優惠規則(打折、滿減、贈品)都必須遵守這個「規矩」,方便后續統一管理。
每個優惠規則都是一個「獨立菜品」(具體策略類)
-
打折策略
:相當于「糖醋排骨」,具體做法是「打 8 折」:
public class DiscountStrategy implements PromotionStrategy {@Overridepublic double calculateDiscount(double orderAmount) {return orderAmount * 0.2; // 直接算優惠金額} }
-
滿減策略
:相當于「魚香肉絲」,具體做法是「滿 200 減 50」:
public class FullReduceStrategy implements PromotionStrategy {@Overridepublic double calculateDiscount(double orderAmount) {return orderAmount >= 200 ? 50 : 0; // 滿足條件才優惠} }
-
贈品策略:相當于「麻婆豆腐」,做法是「滿 100 送贈品」(雖然不直接減錢,但也是一種策略):
public class GiftStrategy implements PromotionStrategy {@Overridepublic double calculateDiscount(double orderAmount) {if (orderAmount >= 100) {System.out.println("送你小風扇!"); // 執行贈品邏輯}return 0; // 金額不變} }
關鍵點:每個策略類都是「自包含」的,就像廚房的不同廚師各自負責一道菜,互相不干擾。
上下文類:相當于「服務員」,負責切換策略
以前沒有上下文類時,你得自己去廚房點菜(業務代碼直接調用策略類),現在有了服務員,你只需要告訴她:「我要吃糖醋排骨」(調用上下文類的方法,傳入策略類型)。
public class OrderContext {private PromotionStrategy currentStrategy; // 當前使用的策略(默認是空的)// 初始化時選一種策略(比如默認用打折)public OrderContext(PromotionStrategy strategy) {this.currentStrategy = strategy;}// 隨時換策略!就像吃飯時突然想換菜,告訴服務員就行public void changeStrategy(PromotionStrategy newStrategy) {this.currentStrategy = newStrategy;}// 計算最終價格:交給當前策略去處理public double calculateFinalPrice(double orderAmount) {return orderAmount - currentStrategy.calculateDiscount(orderAmount);}
}
為什么需要上下文類?
- 解耦調用邏輯:業務代碼不用關心「怎么創建策略對象」,只需要告訴上下文「我要用哪個策略」。
- 支持動態切換:比如用戶下單時先用「打折策略」,付款前突然發現有滿減活動,直接調用
changeStrategy
切換即可,不用改核心計算邏輯。
策略模式+工廠模式
在實際開發的時候呀,很少會只用到一種設計模式,一般都是好幾種設計模式一起用。咱們就拿這個優惠策略的例子來說吧。
這個系統里的優惠策略可不止一種哦。要是以后想再增加新的優惠策略,就會在 PromotionStrategyFactory
這個工廠里創建新的對象。比如說以后又有了新的優惠方式,也得在這個工廠里來創建對應的對象。這里用到了策略模式,只要新的優惠策略實現 PromotionStrategy
這個接口,把里面計算折扣優惠的方法重新寫一下,然后在工廠里把創建這個新策略對象的邏輯加上就行。
下面是 PromotionStrategyFactory
這個工廠類的代碼:
@Component
public class PromotionStrategyFactory {public PromotionStrategy createStrategy(String strategyType) {// 根據傳入的策略類型,用大寫來判斷switch (strategyType.toUpperCase()) {// 如果是 "DISCOUNT",就創建一個折扣策略對象case "DISCOUNT":return new DiscountStrategy();// 如果是 "FULL_REDUCE",就創建一個滿減策略對象case "FULL_REDUCE":return new FullReduceStrategy();// 如果是 "GIFT",就創建一個贈品策略對象case "GIFT":return new GiftStrategy();// 如果傳入的策略類型不認識,就拋出異常default:throw new IllegalArgumentException("未知策略類型: " + strategyType);}}
}
在控制器層,也就是和客戶端交互的地方,代碼是這樣的:
@RestController
@RequestMapping("/api/promotion")
public class PromotionController {// 自動注入訂單上下文對象@Autowiredprivate OrderContext orderContext;// 自動注入優惠策略工廠對象@Autowiredprivate PromotionStrategyFactory strategyFactory;// 處理 GET 請求,計算優惠后的價格@GetMapping("/calculate")public ResponseEntity<Map<String, Object>> calculate(@RequestParam double orderAmount, @RequestParam String strategyType) {try {// 1. 從工廠獲取對應的優惠策略實例PromotionStrategy strategy = strategyFactory.createStrategy(strategyType);// 2. 把獲取到的策略應用到訂單上下文中orderContext.changeStrategy(strategy);// 3. 調用訂單上下文的方法,計算出最終的價格double finalPrice = orderContext.calculateFinalPrice(orderAmount);// 4. 把計算結果放到一個 Map 里,作為響應返回Map<String, Object> result = new LinkedHashMap<>();result.put("開始價格", orderAmount);result.put("折扣類型", strategyType);result.put("最后的折扣", finalPrice);return ResponseEntity.ok(result);} catch (IllegalArgumentException e) {// 如果傳入的策略類型不合法,就返回一個錯誤響應Map<String, Object> error = new LinkedHashMap<>();error.put("error", "INVALID_STRATEGY");error.put("message", e.getMessage());return ResponseEntity.badRequest().body(error);}}
}
簡單來說呢,就是通過工廠類來創建不同的優惠策略對象,然后在控制器里把這些策略應用到訂單上,計算出最終的優惠價格,要是遇到不認識的策略類型,還會給出錯誤提示。
改進(枚舉+sprinboot自動注冊)
在咱們現在用的工廠類里,代碼采用的是硬編碼方式。這就意味著,要是有新的優惠策略類加入,就得去修改工廠方法的代碼。可在開發中,頻繁修改代碼是我們不想看到的,因為這可能會引入新的問題,也不利于代碼的維護和擴展。
簡單工廠模式確實幫我們把客戶端和具體的策略類實現分離開來了,讓它們之間的依賴關系沒那么緊密。不過呢,工廠類在初始化策略對象(也就是策略 beans)的時候,還是和具體的策略類綁得比較緊。也就是說,工廠類得明確知道有哪些具體的策略類,這就導致一旦有新的策略類出現,工廠類就得跟著改。
為了讓它們之間的關系更松散,我們可以借助 Spring 框架里的 InitializingBean
接口和 ApplicationContextAware
接口來自動完成策略對象的裝配工作。
下面來詳細說說這兩個接口的作用:
InitializingBean
接口
InitializingBean
接口有一個 afterPropertiesSet
方法。當 Spring 容器創建并初始化一個實現了 InitializingBean
接口的類的實例時,在設置完所有屬性之后,會自動調用 afterPropertiesSet
方法。我們可以在這個方法里完成一些初始化操作。
ApplicationContextAware
接口
ApplicationContextAware
接口有一個 setApplicationContext
方法。實現了這個接口的類可以通過該方法獲取到 Spring 的 ApplicationContext
(應用上下文)。ApplicationContext
就像是 Spring 容器的大管家,它能管理所有的 Bean,我們可以通過它獲取到容器中所有實現了某個接口的 Bean 實例。
實現自動裝配策略對象
結合這兩個接口,我們可以在工廠類里這樣做:
- 實現
InitializingBean
接口,在afterPropertiesSet
方法中進行策略對象的初始化操作。 - 實現
ApplicationContextAware
接口,通過setApplicationContext
方法獲取ApplicationContext
。 - 使用
ApplicationContext
的getBeansOfType
方法獲取所有實現了PromotionStrategy
接口的 Bean 實例,并將它們存到一個Map
里。
這樣,當有新的策略類添加時,只要它實現了 PromotionStrategy
接口,Spring 容器會自動把它注冊為一個 Bean,afterPropertiesSet
方法會把它添加到 Map
中,工廠類就不需要再手動修改代碼來創建新的策略對象,從而實現了工廠類和具體策略類的解耦。
@Component
public class PromotionFactory implements InitializingBean {private final Map<PromotionType, PromotionStrategy> strategyMap = new HashMap<>();private final ApplicationContext applicationContext;@Autowiredpublic PromotionFactory(ApplicationContext applicationContext) {this.applicationContext = applicationContext;}/*** 初始化:從Spring容器中獲取所有策略Bean,并按枚舉類型注冊* Key:Bean 的名稱(默認是類名首字母小寫,如fullReduceStrategy)。* Value:Bean 的實例(實現了PromotionStrategy接口的具體策略類)。*/@Overridepublic void afterPropertiesSet() {// 獲取所有實現PromotionStrategy接口的BeanMap<String, PromotionStrategy> beans = applicationContext.getBeansOfType(PromotionStrategy.class);beans.forEach((beanName, strategy) -> {// 從策略Bean中獲取對應的枚舉類型(需在策略類中增加獲取類型的方法)// 這里假設策略類通過枚舉類型命名(如FullReduceStrategy對應FULL_REDUCE枚舉)PromotionType type = parseTypeFromBeanName(beanName);if (type != null) {strategyMap.put(type, strategy);}});}/*** 從Bean名稱解析枚舉類型(簡化實現,實際可通過注解或接口方法指定類型)*/private PromotionType parseTypeFromBeanName(String beanName) {try {// Bean名稱默認駝峰式,轉為枚舉大寫return PromotionType.valueOf(beanName.toUpperCase());} catch (IllegalArgumentException e) {return null;}}/*** 獲取策略實例*/public PromotionStrategy getStrategy(PromotionType type) {if (!strategyMap.containsKey(type)) {throw new IllegalArgumentException("未知促銷類型:" + type);}return strategyMap.get(type);}
}
@Getter
public enum PromotionType {FULL_REDUCE("滿減"),DISCOUNT("打折"),GIFT("贈品");private final String desc;PromotionType(String desc) {this.desc = desc;}
}
@RestController
public class TestController {private final OrderService orderService;public TestController(OrderService orderService) {this.orderService = orderService;}/*** 測試接口* http://localhost:8080/calculate?orderAmount=300&promotionType=FULL_REDUCE*/@GetMapping("/calculate")public String calculatePrice(@RequestParam double orderAmount, @RequestParam PromotionType promotionType) {double finalPrice = orderService.calculateFinalPrice(orderAmount, promotionType);return "訂單金額:" + orderAmount + "元,使用" + promotionType.getDesc() + "后,最終價格:" + finalPrice + "元";}
}
調用的模型圖
ice = orderService;
}
/*** 測試接口* http://localhost:8080/calculate?orderAmount=300&promotionType=FULL_REDUCE*/
@GetMapping("/calculate")
public String calculatePrice(@RequestParam double orderAmount, @RequestParam PromotionType promotionType) {double finalPrice = orderService.calculateFinalPrice(orderAmount, promotionType);return "訂單金額:" + orderAmount + "元,使用" + promotionType.getDesc() + "后,最終價格:" + finalPrice + "元";
}
}
調用的模型圖[外鏈圖片轉存中...(img-GYDzYNR0-1748529814600)]