1.概念
参考《JAVA与模式》的描述:
策略模式属于对象的行为模式。其用意是针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。
策略模式使得算法可以在不影响到客户端的情况下发生变化。
2.层次结构
-
目的:策略模式是对算法的包装,是把使用算法的责任和算法本身分割开来,委派给不同的对象管理。
-
实现:策略模式通常把一个系列的算法包装到一系列的策略类里面,通常是作为一个接口的子类。即:“准备一组算法,并将每一个算法封装起来,使得它们可以互换”。
-
UML图如下:
image.png
这个模式涉及到三个角色:
(1)环境(Context)角色:持有一个Strategy的引用。
(2)抽象策略(Strategy)角色:这是一个抽象角色,通常由一个接口或抽象类实
现。此角色给出所有的具体策略类所需的接口。
(3)具体策略(ConcreteStrategy)角色:包装了相关的算法或行为。
3.实际案例
笔者在公司重构金额计算模块时,需要根据产品类型 + 收费代码,计算应收金额
业务逻辑如下:
- 根据产品类型确定:费率rate + 收费规则rule
- 每个产品有001和002两种收费项。根据产品费率rate,去计算这两种收费项的应收金额
原始代码是这样:
// 产品类型
int type = 1;
// 往返都可以直达
if (type == 1) {
// 根据费率rate1 + 收费规则rule1, 计算收费代码为:001,002的款项应收金额
return;
}
// 产品类型为2
if (type == 2) {
// 根据费率rate2 + 收费规则rule2, 计算收费代码为:001,002的款项应收金额
return;
}
// 产品类型为3
if (type == 3) {
// 根据费率rate3 + 收费规则rule3, 计算收费代码为:001,002的款项应收金额
return;
}
// 产品类型为4
else{
// 根据费率rate3 + 收费规则rule3, 计算收费代码为:001,002的款项应收金额
return;
}
4.案例优化
如果我们遇到类似于上面的需求,第一反应肯定是用if-else语句或者switch语句,根据不同的情况执行不同的代码。但是随着需求越来越复杂,这么做的缺陷就慢慢的显现了出来:除了产品类型之外,如果增加了其他判断要素,如:收费周期、标的分类等,那么就会新增分支逻辑进行判断。
随着需求的不断增加,代码的分支会越来越多,代码最终会越来越难以维护,所以策略模式出现了。
4.1原始的策略模式
- 先定义一个model:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Receivables {
private String desc;
}
- 定义计算策略的接口:
public interface CalculateService {
List<Receivables> CalculateReceivables();
}
- 定义不同的策略实现类,有CalculateServiceImpl1,CalculateServiceImpl2,CalculateServiceImpl3,CalculateServiceImpl4(这里只展示一个):
public class CalculateServiceImpl1 implements CalculateService {
@Override
public List<Receivables> CalculateReceivables() {
List<Receivables> list = new ArrayList<>();
list.add(new Receivables("产品1应收金额: 收费项001--金额1000.00"));
list.add(new Receivables("产品1应收金额: 收费项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("产品2应收金额: 收费项001--金额2100.00"));
list.add(new Receivables("产品2应收金额: 收费项002--金额6666.00"));
return list;
}
}
- 在上下文中定义:产品类型 与 计算策略 的映射关系 --- 通过map去记录
并提供 calculate( ) 接口,根据不同的产品类型,去调用不同的应收金额计算策略
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层调用:
@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);
}
}
- 运行结果:
2021-02-16 22:08:03.731 INFO 19176 --- [nio-8080-exec-1] c.q.s.controller.CalculateController : 产品2应收金额: 收费项001--金额2100.00
2021-02-16 22:08:03.732 INFO 19176 --- [nio-8080-exec-1] c.q.s.controller.CalculateController : 产品2应收金额: 收费项002--金额6666.00
4.2与spring结合的策略模式
4.2.1方式1
通过@Autowired + @PostConstruct 注入依赖,并将映射关系加到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;
}
}
运行结果同4.1
4.2.2方式2
- Spring有个技巧:自动注入Map类型
具体说明:直接通过Map<String, CalculateService> 注入依赖,这里:beanName是map中的key,bean对应的接口(实现类)是map中的value
- 首先,@Service注解标明beanName
@Service("2")
public class CalculateServiceImpl2 implements CalculateService {
@Override
public List<Receivables> CalculateReceivables() {
List<Receivables> list = new ArrayList<>();
list.add(new Receivables("产品2应收金额: 收费项001--金额2100.00"));
list.add(new Receivables("产品2应收金额: 收费项002--金额6666.00"));
return list;
}
}
-其次,上下文中自动注入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;
}
}
结果同4.1
5.小结
5.1优点
-
策略的调用和具体策略的实现是松耦合关系。上下文只需要知道要使用哪一个实现Strategy接口的实例,但不需要知道具体是哪一个类。
注:如果这些算法具有公共的功能,可以将接口变为抽象类,将公共功能放到抽象父类里面。
-
策略模式满足“开闭原则”,当增加新的具体策略时,不需要修改上下文类的代码,就可以引用新的策略的实例。
5.2缺点
-
增加了对象的数量
由于策略模式将每个具体的算法都单独封装为一个策略类,如果可选的策略有很多的话,那对象的数量也会很多。 -
只适合偏平的算法结构
由于策略模式的各个策略实现是平等的关系(可相互替换),实际上就构成了一个扁平的算法结构。即一个策略接口下面有多个平等的策略实现,但是运行时只能有一个算法被使用。这就限制了算法的使用层级,且不能被嵌套。
5.3使用场景
- 一个类定义了多种行为,并且这些行为在这个类的方法中以多个 if-else 条件语句的形式出现,那么使用策略模式避免在类中使用大量的条件语句。
- 不希望暴露复杂的,与算法相关的数据结构,那么可以使用策略模式封装算法,即:将算法分别封装到具体策略实现类中。
网友评论