1.概念
參考《JAVA與模式》的描述:
策略模式屬于對(duì)象的行為模式。其用意是針對(duì)一組算法,將每一個(gè)算法封裝到具有共同接口的獨(dú)立的類中,從而使得它們可以相互替換。
策略模式使得算法可以在不影響到客戶端的情況下發(fā)生變化。
2.層次結(jié)構(gòu)
目的:策略模式是對(duì)算法的包裝,是把使用算法的責(zé)任和算法本身分割開來,委派給不同的對(duì)象管理。
實(shí)現(xiàn):策略模式通常把一個(gè)系列的算法包裝到一系列的策略類里面,通常是作為一個(gè)接口的子類。即:“準(zhǔn)備一組算法,并將每一個(gè)算法封裝起來,使得它們可以互換”。
-
UML圖如下:
image.png
這個(gè)模式涉及到三個(gè)角色:
(1)環(huán)境(Context)角色:持有一個(gè)Strategy的引用。
(2)抽象策略(Strategy)角色:這是一個(gè)抽象角色,通常由一個(gè)接口或抽象類實(shí)
現(xiàn)。此角色給出所有的具體策略類所需的接口。
(3)具體策略(ConcreteStrategy)角色:包裝了相關(guān)的算法或行為。
3.實(shí)際案例
筆者在公司重構(gòu)金額計(jì)算模塊時(shí),需要根據(jù)產(chǎn)品類型 + 收費(fèi)代碼,計(jì)算應(yīng)收金額
業(yè)務(wù)邏輯如下:
- 根據(jù)產(chǎn)品類型確定:費(fèi)率rate + 收費(fèi)規(guī)則rule
- 每個(gè)產(chǎn)品有001和002兩種收費(fèi)項(xiàng)。根據(jù)產(chǎn)品費(fèi)率rate,去計(jì)算這兩種收費(fèi)項(xiàng)的應(yīng)收金額
原始代碼是這樣:
// 產(chǎn)品類型
int type = 1;
// 往返都可以直達(dá)
if (type == 1) {
// 根據(jù)費(fèi)率rate1 + 收費(fèi)規(guī)則rule1, 計(jì)算收費(fèi)代碼為:001,002的款項(xiàng)應(yīng)收金額
return;
}
// 產(chǎn)品類型為2
if (type == 2) {
// 根據(jù)費(fèi)率rate2 + 收費(fèi)規(guī)則rule2, 計(jì)算收費(fèi)代碼為:001,002的款項(xiàng)應(yīng)收金額
return;
}
// 產(chǎn)品類型為3
if (type == 3) {
// 根據(jù)費(fèi)率rate3 + 收費(fèi)規(guī)則rule3, 計(jì)算收費(fèi)代碼為:001,002的款項(xiàng)應(yīng)收金額
return;
}
// 產(chǎn)品類型為4
else{
// 根據(jù)費(fèi)率rate3 + 收費(fèi)規(guī)則rule3, 計(jì)算收費(fèi)代碼為:001,002的款項(xiàng)應(yīng)收金額
return;
}
4.案例優(yōu)化
如果我們遇到類似于上面的需求,第一反應(yīng)肯定是用if-else語句或者switch語句,根據(jù)不同的情況執(zhí)行不同的代碼。但是隨著需求越來越復(fù)雜,這么做的缺陷就慢慢的顯現(xiàn)了出來:除了產(chǎn)品類型之外,如果增加了其他判斷要素,如:收費(fèi)周期、標(biāo)的分類等,那么就會(huì)新增分支邏輯進(jìn)行判斷。
隨著需求的不斷增加,代碼的分支會(huì)越來越多,代碼最終會(huì)越來越難以維護(hù),所以策略模式出現(xiàn)了。
4.1原始的策略模式
- 先定義一個(gè)model:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Receivables {
private String desc;
}
- 定義計(jì)算策略的接口:
public interface CalculateService {
List<Receivables> CalculateReceivables();
}
- 定義不同的策略實(shí)現(xiàn)類,有CalculateServiceImpl1,CalculateServiceImpl2,CalculateServiceImpl3,CalculateServiceImpl4(這里只展示一個(gè)):
public class CalculateServiceImpl1 implements CalculateService {
@Override
public List<Receivables> CalculateReceivables() {
List<Receivables> list = new ArrayList<>();
list.add(new Receivables("產(chǎn)品1應(yīng)收金額: 收費(fèi)項(xiàng)001--金額1000.00"));
list.add(new Receivables("產(chǎn)品1應(yīng)收金額: 收費(fèi)項(xiàng)002--金額2000.00"));
return list;
}
}
@Service
public class CalculateServiceImpl2 implements CalculateService {
@Override
public List<Receivables> CalculateReceivables() {
List<Receivables> list = new ArrayList<>();
list.add(new Receivables("產(chǎn)品2應(yīng)收金額: 收費(fèi)項(xiàng)001--金額2100.00"));
list.add(new Receivables("產(chǎn)品2應(yīng)收金額: 收費(fèi)項(xiàng)002--金額6666.00"));
return list;
}
}
- 在上下文中定義:產(chǎn)品類型 與 計(jì)算策略 的映射關(guān)系 --- 通過map去記錄
并提供 calculate( ) 接口,根據(jù)不同的產(chǎn)品類型,去調(diào)用不同的應(yīng)收金額計(jì)算策略
public class CalculateStrategy {
private static Map<Integer, CalculateService> strategyMap = new HashMap<>();
static {
strategyMap.put(1, new CalculateServiceImpl1());
strategyMap.put(2, new CalculateServiceImpl2());
strategyMap.put(3, new CalculateServiceImpl3());
strategyMap.put(4, new CalculateServiceImpl4());
}
public List<String> calculate(Integer type) {
List<Receivables> receivablesList = strategyMap.get(type).CalculateReceivables();
List<String> collect = receivablesList.stream()
.map(Receivables::getDesc)
.collect(Collectors.toList());
return collect;
}
}
- controller層調(diào)用:
@RestController
@RequestMapping("/calculate")
@Slf4j
public class CalculateController {
private static final Logger logger = LoggerFactory.getLogger(CalculateController.class);
@Autowired
private CalculateStrategy calculateStrategy;
@GetMapping("/strategy-test")
public void test(@RequestParam("type") String type) {
List<String> calculate = calculateStrategy.calculate(Integer.parseInt(type));
calculate.forEach(logger::info);
}
}
- 運(yùn)行結(jié)果:
2021-02-16 22:08:03.731 INFO 19176 --- [nio-8080-exec-1] c.q.s.controller.CalculateController : 產(chǎn)品2應(yīng)收金額: 收費(fèi)項(xiàng)001--金額2100.00
2021-02-16 22:08:03.732 INFO 19176 --- [nio-8080-exec-1] c.q.s.controller.CalculateController : 產(chǎn)品2應(yīng)收金額: 收費(fèi)項(xiàng)002--金額6666.00
4.2與spring結(jié)合的策略模式
4.2.1方式1
通過@Autowired + @PostConstruct 注入依賴,并將映射關(guān)系加到strategyMap中
@Service
public class CalculateStrategy {
@Autowired
private CalculateServiceImpl1 calculateServiceImpl1;
@Autowired
private CalculateServiceImpl2 calculateServiceImpl2;
@Autowired
private CalculateServiceImpl3 calculateServiceImpl3;
@Autowired
private CalculateServiceImpl4 calculateServiceImpl4;
private static Map<Integer, CalculateService> strategyMap = new HashMap<>();
@PostConstruct
public void init() {
strategyMap.put(1, calculateServiceImpl1);
strategyMap.put(2, calculateServiceImpl2);
strategyMap.put(3, calculateServiceImpl3);
strategyMap.put(4, calculateServiceImpl4);
}
// static {
// strategyMap.put(1, calculateServiceImpl1);
// strategyMap.put(2, calculateServiceImpl2);
// strategyMap.put(3, calculateServiceImpl3);
// strategyMap.put(4, calculateServiceImpl4);
// }
public List<String> calculate(Integer type) {
List<Receivables> receivablesList = strategyMap.get(type).CalculateReceivables();
List<String> collect = receivablesList.stream()
.map(Receivables::getDesc)
.collect(Collectors.toList());
return collect;
}
}
運(yùn)行結(jié)果同4.1
4.2.2方式2
- Spring有個(gè)技巧:自動(dòng)注入Map類型
具體說明:直接通過Map<String, CalculateService> 注入依賴,這里:beanName是map中的key,bean對(duì)應(yīng)的接口(實(shí)現(xiàn)類)是map中的value
- 首先,@Service注解標(biāo)明beanName
@Service("2")
public class CalculateServiceImpl2 implements CalculateService {
@Override
public List<Receivables> CalculateReceivables() {
List<Receivables> list = new ArrayList<>();
list.add(new Receivables("產(chǎn)品2應(yīng)收金額: 收費(fèi)項(xiàng)001--金額2100.00"));
list.add(new Receivables("產(chǎn)品2應(yīng)收金額: 收費(fèi)項(xiàng)002--金額6666.00"));
return list;
}
}
-其次,上下文中自動(dòng)注入Map<String, CalculateService>
@Service
public class CalculateStrategy {
@Autowired
private Map<String, CalculateService> strategyMap;
public List<String> calculate(Integer type) {
List<Receivables> receivablesList = strategyMap.get(String.valueOf(type)).CalculateReceivables();
List<String> collect = receivablesList.stream()
.map(Receivables::getDesc)
.collect(Collectors.toList());
return collect;
}
}
結(jié)果同4.1
5.小結(jié)
5.1優(yōu)點(diǎn)
-
策略的調(diào)用和具體策略的實(shí)現(xiàn)是松耦合關(guān)系。上下文只需要知道要使用哪一個(gè)實(shí)現(xiàn)Strategy接口的實(shí)例,但不需要知道具體是哪一個(gè)類。
注:如果這些算法具有公共的功能,可以將接口變?yōu)槌橄箢悾瑢⒐补δ芊诺匠橄蟾割惱锩妗?/strong>
策略模式滿足“開閉原則”,當(dāng)增加新的具體策略時(shí),不需要修改上下文類的代碼,就可以引用新的策略的實(shí)例。
5.2缺點(diǎn)
-
增加了對(duì)象的數(shù)量
由于策略模式將每個(gè)具體的算法都單獨(dú)封裝為一個(gè)策略類,如果可選的策略有很多的話,那對(duì)象的數(shù)量也會(huì)很多。 -
只適合偏平的算法結(jié)構(gòu)
由于策略模式的各個(gè)策略實(shí)現(xiàn)是平等的關(guān)系(可相互替換),實(shí)際上就構(gòu)成了一個(gè)扁平的算法結(jié)構(gòu)。即一個(gè)策略接口下面有多個(gè)平等的策略實(shí)現(xiàn),但是運(yùn)行時(shí)只能有一個(gè)算法被使用。這就限制了算法的使用層級(jí),且不能被嵌套。
5.3使用場(chǎng)景
- 一個(gè)類定義了多種行為,并且這些行為在這個(gè)類的方法中以多個(gè) if-else 條件語句的形式出現(xiàn),那么使用策略模式避免在類中使用大量的條件語句。
- 不希望暴露復(fù)雜的,與算法相關(guān)的數(shù)據(jù)結(jié)構(gòu),那么可以使用策略模式封裝算法,即:將算法分別封裝到具體策略實(shí)現(xiàn)類中。
