1 什么是策略模式
策略模式(Strategy Pattern)是一种常用的面向对象设计模式,它定义了一系列可互相替换的算法或策略,并将每个算法封装成独立的对象,使得它们可以在运行时动态地替换。具体来说,策略模式定义了一系列算法,每个算法都封装在一个具体的策略类中,这些策略类实现了相同的接口或抽象类。在使用算法的时候,客户端通过一个上下文对象来调用策略类的方法,从而完成算法的执行。这样,客户端可以在运行时动态地选择不同的策略类,从而实现不同的行为。用一句话来说,就是:“准备一组算法,并将每一个算法封装起来,使得它们可以互换”。
这个模式涉及到三个角色:
● 环境(Context)角色:持有一个Strategy的引用。
● 抽象策略(Strategy)角色:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
● 具体策略(ConcreteStrategy)角色:包装了相关的算法或行为。
2 认识策略模式
策略模式的重心:
策略模式的重心不是如何实现算法,而是如何组织、调用这些算法,从而让程序结构更灵活,具有更好的维护性和扩展性。
算法的平等性:
策略模式一个很大的特点就是各个策略算法的平等性。对于一系列具体的策略算法,大家的地位是完全一样的,正因为这个平等性,才能实现算法之间可以相互替换。所有的策略算法在实现上也是相互独立的,相互之间是没有依赖的。
所以可以这样描述这一系列策略算法:策略算法是相同行为的不同实现。
运行时策略的唯一性:
运行期间,策略模式在每一个时刻只能使用一个具体的策略实现对象,虽然可以动态地在不同的策略实现中切换,但是同时只能使用一个。
公有的行为:
经常见到的是,所有的具体策略类都有一些公有的行为。这时候,就应当把这些公有的行为放到共同的抽象策略角色Strategy类里面。当然这时候抽象策略角色必须要用Java抽象类实现,而不能使用接口。
3 策略模式的优点和缺点
策略模式的优点
(1)策略模式提供了管理相关的算法族的办法。策略类的等级结构定义了一个算法或行为族。恰当使用继承可以把公共的代码移到父类里面,从而避免代码重复。
(2)使用策略模式可以避免使用多重条件(if-else)语句。多重条件语句不易维护,它把采取哪一种算法或采取哪一种行为的逻辑与算法或行为的逻辑混合在一起,统统列在一个多重条件语句里面,比使用继承的办法还要原始和落后。
策略模式的缺点
(1)客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法类。换言之,策略模式只适用于客户端知道算法或行为的情况。
(2)由于策略模式把每个具体的策略实现都单独封装成为类,如果备选的策略很多的话,那么对象的数目就会很可观。
4 策略模式适用场景:
(1) 当一个系统中存在多个类只有它们的行为或算法不同时。
(2) 当一个类定义了多种行为,而这些行为在这个类的操作中以多个条件语句的形式出现,可以将相关的条件分支移入它们各自的策略类中,以替换这些条件语句。
(3) 当系统需要动态地在几种算法中选择一种时,如根据不同的配置、用户选择或者环境条件等。
5 策略模式的最佳实践(重点)
策略模式是一个非常简单且常用的设计模式,策略模式最常见的作用就是解决代码中冗长的 if-else 或 switch 分支判断语句;同时也可以提高程序的扩展性和灵活性,避免代码重复
策略模式的使用包含三部分:策略的定义、创建和使用。
5.1 策略的定义、创建和使用
5.1.1 策略的定义
策略类的定义比较简单,包含一个策略接口和一组实现这个接口的策略类。所有的策略类都实现相同的接口,所以,客户端代码基于接口而非实现编程,可以灵活地替换不同的策略。
public interface Strategy { void algorithmInterface(); } public class ConcreteStrategyA implements Strategy { @Override public void algorithmInterface() { //具体的算法... } } public class ConcreteStrategyB implements Strategy { @Override public void algorithmInterface() { //具体的算法... } } |
如上述代码所示,定义了一个策略接口 Strategy,具体的策略实现类都实现 Strategy 接口,并重写 algorithmInterface 方法,最后客户端根据不同的策略即可调用不同实现类的 algorithmInterface 方法。
5.1.2 策略的创建
可以将创建策略的代码逻辑抽象到工厂类中,提前在工厂类创建好所有策略类,缓存在 Map 中。Map 的 key 为策略类型,value 为具体的策略实现类。当需要使用策略时根据 type 去 Map 中 get 即可获取到相应的策略实现类。
public class StrategyFactory { // Map 的 key 为策略类型,value 为具体的策略实现类 private static final Map<String, Strategy> strategies = new HashMap<>(); // 提前创建好所有策略类,缓存到 Map 中 static { strategies.put("A", new ConcreteStrategyA()); strategies.put("B", new ConcreteStrategyB()); } // 需要使用策略时根据 type 去 Map 中 get 即可获取到相应的策略实现类 public static Strategy getStrategy(String type) { if (type == null || type.isEmpty()) { throw new IllegalArgumentException("type should not be empty."); } return strategies.get(type); } }
这里重点在于使用”查表法”代替了大量的分支判断,即每次根据 type 去 Map 中获取,省略了大量的 if-else。
5.1.3 策略的使用
具体使用 A、B、C 何种策略,在具体的场景,可以会根据系统的配置来选择。可以从配置文件中读取出配置,然后传递给策略工厂类 StrategyFactory 的 getStrategy 方法即可获取到相应的策略类。最后调用策略类的 algorithmInterface 方法去执行代码逻辑。
代码如下所示,省略了从配置文件中读取配置的流程。
// 根据 type 的不同,执行不同分支的代码逻辑, private void process(String type){ Strategy strategy = StrategyFactory.getStrategy(type); // 调用策略类的 algorithmInterface 方法去执行代码逻辑 strategy.algorithmInterface(); } |
5.1.4 当前设计是否容易扩展性
最原始的 process 方法中,首先会判断各种 type,然后执行不同类型的代码逻辑。如果需要扩展新的 D、E、F 类型,需要大量修改 process 方法。
优化后,如果需要扩展新的 D、E、F 类型,流程如下:
- 定义相应的 D、E、F 类型的策略实现类
- 提前在策略工厂类 StrategyFactory 中创建相应的策略实现类,并添加到 Map 中
- 客户端代码不用进行任何改动,即:process 方法不需要进行改动
优化后的代码相对来说职责更加单一,且对调用方非常友好。调用方代码不需要任何改动即可使用新的策略。要做的可能就是在配置文件中配置新的策略即可。
5.2 有状态的策略类不能提前创建
在策略类的创建部分,在类初始化时,将所有的策略类提前创建好,存放在 Map 中。当需要使用策略时根据 type 去 Map 中 get 即可获取到相应的策略实现类。
假设策略类是有状态的,每次获取策略对象时,都要求创建新的策略类。此时,就不能使用 Map 缓存的方式来优化代码结构了。可以使用如下方式实现策略工厂类:
public class StrategyFactory { public static Strategy getStrategy(String type) { if (type == null || type.isEmpty()) { throw new IllegalArgumentException("type should not be empty."); } if (type.equals("A")) { return new ConcreteStrategyA(); } else if (type.equals("B")) { return new ConcreteStrategyB(); } return null; } } |
上述代码又退化回了 if-else 嵌套,当然也可以优化为 switch-case 的设计。
极客时间-王争老师的《设计模式之美》课程第 60 节最后的课堂讨论中留下了一道题目:在策略工厂类中,如果每次都要返回新的策略对象,我们还是需要在工厂类中编写 if-else 分支判断逻辑,那这个问题该如何解决呢?
笔者看到文末大家的回答,点赞数最高的评论是:仍然可以用查表法,只不过存储的不再是实例,而是class,使用时获取对应的class,再通过反射创建实例。
反射在这里应该是可以实现,但是笔者感觉不是非常灵活,假设策略实现类需要在这里调用一些有参构造器,且不同的策略类的有参构造器需要传入的参数不同,那么反射实现起来不是非常灵活。
例如 ConcreteStrategyA 的构造器需要传入 age,ConcreteStrategyB 的构造器需要传入 date。对于这样的 case,反射不太好实现,如果实现出来,也是一对 if-else 分支判断。
文末也没有其他令笔者眼前一亮的回答,反倒是笔者在阅读 Flink 源码的过程中,发现了一个笔者感觉比较优秀的解决方案,下面就到了秀操作环节。
5.2.1 秀操作
首先分析一个问题来源:
- 对于无状态的策略类,将所有的策略类提前创建好,存放在 Map 中。当需要使用策略时根据 type 去 Map 中 get 即可获取到相应的策略实现类。
- 对于有状态的策略类,不能提前创建所有的策略类,所以没办法提前创建好将其存放在 Map 中
换种思路:
- 给每个具体的策略类创建相应的策略类工厂。例如 ConcreteStrategyA 的工厂为 StrategyFactoryA, ConcreteStrategyB 的工厂为 StrategyFactoryB。
- 虽然没办法提前创建好策略类放到 Map 中,但是可以将策略类的工厂类提前创建好放到 Map 中。根据传入的 type 就可以从 Map 中获取相应策略类的工厂类,然后执行工厂类的 create 方法即可创建出相应的策略类。
根据上述思路,实现相应代码。
首先定义策略工厂接口,并分别实现策略 A 和策略 B 的工厂类:
// 策略工厂接口 public interface StrategyFactory { Strategy create(); } // 策略 A 的工厂类,用于创建策略 A public class StrategyFactoryA implements StrategyFactory{ @Override public Strategy create() { return new ConcreteStrategyA(); } } // 策略 B 的工厂类,用于创建策略 B public class StrategyFactoryB implements StrategyFactory{ @Override public Strategy create() { return new ConcreteStrategyB(); } } |
对外开放的工厂实现如下:
public class Factory { // Map 的 key 为策略类型,value 为 策略的工厂类 private static final Map<String, StrategyFactory> STRATEGY_FACTORIES = new HashMap<>(); static { // 将各种实现类的工厂提前创建好放到 Map 中 STRATEGY_FACTORIES.put("A", new StrategyFactoryA()); STRATEGY_FACTORIES.put("B", new StrategyFactoryB()); } public static Strategy getStrategy(String type) { if (type == null || type.isEmpty()) { throw new IllegalArgumentException("type should not be empty."); } // 根据 type 获取对应的策略工厂 StrategyFactory strategyFactory = STRATEGY_FACTORIES.get(type); // 调用具体工厂类的 create 方法即可创建出相应的策略类 return strategyFactory.create(); } } |
Factory 类中定义了 Map,Map 的 key 为策略类型,value 为 策略的工厂类。Factory 类初始化时,将各种实现类的工厂提前创建好放到 Map 中。
Factory 类的静态方法 getStrategy 用于根据 type 创建相应的策略类,getStrategy 方法根据 type 从 Map 中获取 type 对应的策略类的工厂,调用具体工厂类的 create 方法即可创建出相应的策略类。
当策略类的构造方法比较复杂也没关系,封装在策略类相应的工厂中即可。
旧方案对于每次要创建新策略类的场景,要搞一堆 if-else 分支判断,上述流程使用 Map 优化了 if-else 分支判断逻辑。但带来了一个新的问题,即:创建出了很多类,相比之前的实现来讲,多了 StrategyFactoryA 和 StrategyFactoryB 类。
为了代码的简洁,可以利用 Java8 的 lambda 表达式将 StrategyFactoryA 和 StrategyFactoryB 类优化掉。截取上述部分代码实现:
// 策略工厂接口 public interface StrategyFactory { Strategy create(); } // 策略 A 的工厂类,用于创建策略 A public class StrategyFactoryA implements StrategyFactory{ @Override public Strategy create() { return new ConcreteStrategyA(); } } Map<String, StrategyFactory> STRATEGY_FACTORIES = new HashMap<>(); // 将各种实现类的工厂提前创建好放到 Map 中 STRATEGY_FACTORIES.put("A", new StrategyFactoryA()); |
上述代码使用 lambda 优化后:
// 策略工厂接口 public interface StrategyFactory { Strategy create(); } Map<String, StrategyFactory> STRATEGY_FACTORIES = new HashMap<>(); // 将各种实现类的工厂提前创建好放到 Map 中 STRATEGY_FACTORIES.put("A", () -> new ConcreteStrategyA()); |
关于 lambda 这里就不多解释了。lambda 表达式还能优化为 Java8 的方法引用,代码如下所示:
STRATEGY_FACTORIES.put(“A”, ConcreteStrategyA::new);
5.2.1 小结
把上述整个代码的最终版贴在这里:
// 策略工厂接口 public interface StrategyFactory { Strategy create(); } public class Factory { // Map 的 key 为策略类型,value 为 策略的工厂类 private static final Map<String, StrategyFactory> STRATEGY_FACTORIES = new HashMap<>(); static { // 将各种实现类的工厂提前创建好放到 Map 中 STRATEGY_FACTORIES.put("A", ConcreteStrategyA::new); STRATEGY_FACTORIES.put("B", ConcreteStrategyB::new); } public static Strategy getStrategy(String type) { if (type == null || type.isEmpty()) { throw new IllegalArgumentException("type should not be empty."); } // 根据 type 获取对应的策略工厂 StrategyFactory strategyFactory = STRATEGY_FACTORIES.get(type); // 调用具体工厂类的 create 方法即可创建出相应的策略类 return strategyFactory.create(); } } |
代码量相比之前的策略类可以共享的代码设计来讲,只是增加了一个 StrategyFactory 接口的设计,所以整体代码也是非常简洁的。
5.3 策略模式在项目中的真实实现
5.3.1 实际需求
智慧停车的功能模块,由于各个地方项目上用到的第三方车厂厂商的设备不一致(如 科托,捷顺,红门,零壹等)对应需要要对接的接口也比较多;虽然对接的车厂不一样,但是在我们业务系统中要做的事都是一样的;如都需要通过我们app来开通月卡,续费,扫码出场,车辆预约等;只是这些功能的实现需要去调用不同车厂的提供的相似功能的接口去实现;
该系统需要动态地在几种算法中选择一种时,如根据不同的配置来选择;因此选择策略模式是比较合适的;这样可以程序结构更灵活,具有更好的维护性和扩展性(后续如果要加入其他的第三方车厂就很方便拓展了);策略类的等级结构定义了一个算法或行为族。恰当使用继承可以把公共的代码移到父类里面,从而避免代码重复.
5.3.2 实践
策略的定义
策略类的定义比较简单,包含一个策略接口和一组实现这个接口的策略类。所有的策略类都实现相同的接口,所以,客户端代码基于接口而非实现编程,可以灵活地替换不同的策略。
public interface ThirdParkVersionService {
/**--------------------------------科托车场厂商相关接口对接--------------------------------------------*/
KeyTopResp<CarsCardVOs> getCarCardList(CarCardDTOs dto);
/**--------------------------------捷顺车场厂商相关接口对接--------------------------------------------*/
JsstResp<DelaybymodeVO> delaybymode(DelaybymodeDTO delaybymodeDTO);
}
/**
* @Description 聚合不同厂商的默认实现
* @version1.0 利用接口默认方法进行空实现,防止下层接口需要实现过多方法
*/
public interface ThirdParkDefaultService extends ThirdParkVersionService{
static String getThirdConfigKey(String thirdParkId,Long projectId){
return projectId.toString().concat(thirdParkId);
}
/**--------------------------------------科托厂商接口默认实现-------------------------------------------------*/
/**
* @Description:
* @Param: [carCardDTOs]
* @return: com.bzcst.bop.iot.car.manager.entity.common.response.KeyTopResp<com.bzcst.bop.iot.car.manager.entity.keytopvo.CarsCardVOs>
* @Author: xiongguoshuang
* @Exception: 获取固定车列表
* @Date: 2022/5/9 13:51
*/
@Override
default KeyTopResp<CarsCardVOs> getCarCardList(CarCardDTOs dto) {
return null;
}
......................
}
public interface KeyTopService extends ThirdParkDefaultService{
//实现科托相关接口
}
public interface JsstService extends ThirdParkDefaultService {
// 实现捷顺相关接口
}
@Component(ThirdParkConstants.BUSINESS_TYPE_KEYTOP_VER_FIVE)
public class KeyTopFiveServiceImpl implements KeyTopService{
//接口实现类
}
@Component(ThirdParkConstants.BUSINESS_TYPE_JSST_VER_THIRD)
public class JsstThirdServiceImpl implements JsstService {
//接口实现类
}
策略的创建
可以将创建策略的代码逻辑抽象到工厂类中,提前在工厂类创建好所有策略类,缓存在 Map 中。Map 的 key 为策略类型,value 为具体的策略实现类。当需要使用策略时根据 type 去 Map 中 get 即可获取到相应的策略实现类。这里的Map 通过服务类自定义的类名自动注入实现
@Component
public class ThirdParkFactorySelector {
@Resource
private Map<String, ThirdParkFactoryService> handlers;
public ThirdParkFactoryService thirdParkFactoySelector(String providerCode) {
ThirdParkFactoryService thirdParkFactoryService = handlers.get(providerCode);
return thirdParkFactoryService;
}
}
@Component
public class ThirdParkFactoryServiceSelector {
@Resource
DeviceProviderApiService deviceProviderApiService;
@Resource
CarYardConfigService carYardConfigService;
@Resource
ThirdParkFactorySelector thirdParkFactorySelector;
/**
* 根据车场ID获取对应的车场厂商实例 调用需要实现的业务逻辑
*/
public ThirdParkFactoryService getThirdParkFactoryService(Long carParkId) {
CarYardConfig carYardConfig = getCarYardConfigByParkId(carParkId);
Long partnerId = carYardConfig.getPartnerId();
if(partnerId == null){
throw new BusinessException("根据carParkId未找到对应的partnerId");
}
DeviceProviderVO provider = deviceProviderApiService.get(partnerId);
return thirdParkFactorySelector.thirdParkFactoySelector(provider.getCode());
}
}
策略的使用
具体在业务代码中调用那个车厂厂商的接口,是根据系统配置表中具体的配置来选择,从数据表中读取具体的配置,调用getThirdParkFactoryService获取对应的策略类;最后调用策略类的中具体的方法去执行代码逻辑。