爲什麼我不再推薦枚舉策略模式?
一、爲什麼講策略模式
策略模式,應該是工作中比較常用的設計模式,調用方自己選擇用哪一種策略完成對數據的操作,也就是 “一個類的行爲或其算法可以在運行時更改”
我個人的理解是 將一些除了過程不同其他都一樣的函數封裝成策略,然後調用方自己去選擇想讓數據執行什麼過程策略。常見的例子爲根據用戶分類推薦不同的排行榜(用戶關注點不一樣,推薦榜單就不一樣)
和單例模式一樣,隨着時間發展,我不再推薦經典策略模式,更推薦簡單策略用枚舉策略模式,複雜地用工廠策略模式。下面引入一個例子,我們的需求是:對一份股票數據列表,給出低價榜、高價榜、漲幅榜。這其中只有排序條件的區別,比較適合作爲策略模式的例子
二、經典策略模式
數據 DTO
@Data
public class Stock {
// 股票交易代碼
private String code;
// 現價
private Double price;
// 漲幅
private Double rise;
}
抽象得到的策略接口
public interface Strategy {
/**
* 將股票列表排序
*
* @param source 源數據
* @return 排序後的榜單
*/
List<Stock> sort(List<Stock> source);
}
實現我們的策略類
/**
* 高價榜
*/
public class HighPriceRank implements Strategy {
@Override
public List<Stock> sort(List<Stock> source) {
return source.stream()
.sorted(Comparator.comparing(Stock::getPrice).reversed())
.collect(Collectors.toList());
}
}
/**
* 低價榜
*/
public class LowPriceRank implements Strategy {
@Override
public List<Stock> sort(List<Stock> source) {
return source.stream()
.sorted(Comparator.comparing(Stock::getPrice))
.collect(Collectors.toList());
}
}
/**
* 高漲幅榜
*/
public class HighRiseRank implements Strategy {
@Override
public List<Stock> sort(List<Stock> source) {
return source.stream()
.sorted(Comparator.comparing(Stock::getRise).reversed())
.collect(Collectors.toList());
}
}
經典的 Context 類,
public class Context {
private Strategy strategy;
public void setStrategy(Strategy strategy) {
this.strategy = strategy;
}
public List<Stock> getRank(List<Stock> source) {
return strategy.sort(source);
}
}
於是 我們順禮成章地得到調用類 -- 榜單實例 RankServiceImpl
@Service
public class RankServiceImpl {
/**
* dataService.getSource() 提供原始的股票數據
*/
@Resource
private DataService dataService;
/**
* 前端傳入榜單類型, 返回排序完的榜單
*
* @param rankType 榜單類型
* @return 榜單數據
*/
public List<Stock> getRank(String rankType) {
// 創建上下文
Context context = new Context();
// 這裏選擇策略
switch (rankType) {
case "HighPrice":
context.setStrategy(new HighPriceRank());
break;
case "LowPrice":
context.setStrategy(new LowPriceRank());
break;
case "HighRise":
context.setStrategy(new HighRiseRank());
break;
default:
throw new IllegalArgumentException("rankType not found");
}
// 然後執行策略
return context.getRank(dataService.getSource());
}
}
我們可以看到經典方法,創建了一個接口、三個策略類,還是比較囉嗦的。調用類的實現也待商榷,新增一個策略類還要修改榜單實例(可以用抽象工廠解決,但是複雜度又上升了)。加之我們有更好的選擇,所以此處不再推薦經典策略模式
三、基於枚舉的策略模式
這裏對這種簡單的策略,推薦用枚舉進行優化。枚舉的本質是創建了一些靜態類的集合。
我下面直接給出例子,大家可以直觀感受一下
枚舉策略類
public enum RankEnum {
// 以下三個爲策略實例
HighPrice {
@Override
public List<Stock> sort(List<Stock> source) {
return source.stream()
.sorted(Comparator.comparing(Stock::getPrice).reversed())
.collect(Collectors.toList());
}
},
LowPrice {
@Override
public List<Stock> sort(List<Stock> source) {
return source.stream()
.sorted(Comparator.comparing(Stock::getPrice))
.collect(Collectors.toList());
}
},
HighRise {
@Override
public List<Stock> sort(List<Stock> source) {
return source.stream()
.sorted(Comparator.comparing(Stock::getRise).reversed())
.collect(Collectors.toList());
}
};
// 這裏定義了策略接口
public abstract List<Stock> sort(List<Stock> source);
}
對應的調用類也得以優化,榜單實例 RankServiceImpl
@Service
public class RankServiceImpl {
/**
* dataService.getSource() 提供原始的股票數據
*/
@Resource
private DataService dataService;
/**
* 前端傳入榜單類型, 返回排序完的榜單
*
* @param rankType 榜單類型 形似 RankEnum.HighPrice.name()
* @return 榜單數據
*/
public List<Stock> getRank(String rankType) {
// 獲取策略,這裏如果未匹配會拋 IllegalArgumentException異常
RankEnum rank = RankEnum.valueOf(rankType);
// 然後執行策略
return rank.sort(dataService.getSource());
}
}
可以看到,如果策略簡單的話,基於枚舉的策略模式優雅許多,調用方也做到了 0 修改,但正確地使用枚舉策略模式需要額外考慮以下幾點。
-
枚舉的策略類是公用且靜態,這意味着這個策略過程不能引入非靜態的部分,擴展性受限
-
策略模式的目標之一,是優秀的擴展性和可維護性,最好能新增或修改某一策略類時,對其他類是無改動的。而枚舉策略如果過多或者過程複雜,維護是比較困難的,可維護性受限
四、基於工廠的策略模式
爲了解決良好的擴展性和可維護性,我更推薦以下利用 spring 自帶 beanFactory 的優勢,實現一個基於工廠的策略模式。
策略類改動只是添加了 @Service 註解,並指定了 Service 的 value 屬性
/**
* 高價榜
* 注意申明 Service.value = HighPrice,他是我們的key,下同
*/
@Service("HighPrice")
public class HighPriceRank implements Strategy {
@Override
public List<Stock> sort(List<Stock> source) {
return source.stream()
.sorted(Comparator.comparing(Stock::getPrice).reversed())
.collect(Collectors.toList());
}
}
/**
* 低價榜
*/
@Service("LowPrice")
public class LowPriceRank implements Strategy {
@Override
public List<Stock> sort(List<Stock> source) {
return source.stream()
.sorted(Comparator.comparing(Stock::getPrice))
.collect(Collectors.toList());
}
}
/**
* 高漲幅榜
*/
@Service("HighRise")
public class HighRiseRank implements Strategy {
@Override
public List<Stock> sort(List<Stock> source) {
return source.stream()
.sorted(Comparator.comparing(Stock::getRise).reversed())
.collect(Collectors.toList());
}
}
調用類修改較大,接入藉助 spring 工廠特性,完成策略類
@Service
public class RankServiceImpl {
/**
* dataService.getSource() 提供原始的股票數據
*/
@Resource
private DataService dataService;
/**
* 利用註解@Resource和@Autowired特性,直接獲取所有策略類
* key = @Service的value
*/
@Resource
private Map<String, Strategy> rankMap;
/**
* 前端傳入榜單類型, 返回排序完的榜單
*
* @param rankType 榜單類型 和Service註解的value屬性一致
* @return 榜單數據
*/
public List<Stock> getRank(String rankType) {
// 判斷策略是否存在
if (!rankMap.containsKey(rankType)) {
throw new IllegalArgumentException("rankType not found");
}
// 獲得策略實例
Strategy rank = rankMap.get(rankType);
// 執行策略
return rank.sort(dataService.getSource());
}
}
若讀者使用的不是 Spring,也可以找找對應框架的工廠模式實現,或者自己實現一個抽象工廠。
工廠策略模式會比枚舉策略模式囉嗦,但也更加靈活、易擴展性和易維護。故簡單策略推薦枚舉策略模式,複雜策略才推薦工廠策略模式。
來源:
https://www.toutiao.com/article/7080135241830302212/?log_from=96ec694bd1284_1648603798564
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/pu1MfY4cX7soVLCe2_40OA