前言

项目中有这样一个场景,在公园放置了用来拍摄人像的识别杆,根据用户在不同识别杆之间采集的图象来计算用户的运动距离。由于涉及到许多公园,每个公园的布局不同,识别杆之间距离不同,算法也不同。但代码中每个不同的公园的算法区别都采用ifelse来进行判断处理。

这样的写法你能看得下去吗?肯定不能。所以,就用策略模式对此进行了重构。项目采用SpringBoot架构,于是对不同的策略模式写法又进行了一次升级。现在就以实战的角度带领大家来学习策略模式,以及如何将ifelse重构为基于SpringBoot的策略模式。

ifelse的伪代码

由于业务逻辑比较复杂,这里以最简单的简化模型来为大家展示一段伪代码。

public int getDistance(int parkId, int count) {// 如果是公园x,1个间距为y(比如:10)米if (parkId == 1) {return 10 * count;} else if (parkId == 2) {return 5 * count;} else if (parkId == 3) {return 50 * count;} else {// 默认 20米return 20 * count;}
}

假设上面的代码是用来计算不同识别杆之间的距离的。不同的公园识别杆距离不同,根据parkId来计算多个识别杆之间的距离。当然真实业务不可能这么简单的相乘。

首先对照一下设计模式的开闭原则:面对扩展开放,面对修改关闭。

上述代码,如果某个公园的计算算法改变了,那么这段代码就要进行修改,或者如果新增了一个公园,这段代码同样需要修改。一旦修改必然会影响到其他公园的业务逻辑。完全不符合开闭原则,同时代码中还充斥着大量的ifelse,如果业务复杂,代码会急速膨胀。

那么,下面我们就针对以上实例,用策略模式来进行重新设计。

什么是策略模式

策略模式属于对象的行为模式,是针对算法的包装。通常场景为,对象有某个行为,但是在不同的场景中,该行为有不同的实现算法。

那么针对这一组算法,将每一个实现封装为抽象策略类的子类,从而使得它们可以相互替换。策略模式使得算法可以在不影响到客户端的情况下发生变化。

策略模式的结构

如果用类图来表示,策略模式的结构可以展示如下:

策略类简单的来说,就是定义一个接口或抽象类,在其中定义公共算法方法,然后不同的子类实现该接口的方法。同时针对接口的操作和获取又有一个环境(Context)类来进行辅助或封装。

在上图中各部分的角色功能如下:

  • 环境(Context)角色:持有Strategy的引用,通常会封装一些获取实现类的方法,或策略类的调用。相对来说定义比较灵活。
  • 抽象策略(Strategy)角色:抽象角色,通常是一个接口或抽象类。所有的具体策略类都需要实现该接口。如果是抽象类,可实现一些公共的方法。
  • 具体策略(ConcreteStrategy)角色:具体算法或行为的实现,也就是抽象策略类中定义的方法的不同实现。

简单代码重构

首先定义一个计算距离的抽象类AbstractParkStrategy。实战过程中可根据具体情况采用接口或抽象类。

/*** 抽象类,此处也可以使用接口,根据具体情况定义**/
public abstract class AbstractParkStrategy {/*** 计算距离的抽象方法* @param count 节点数* @return 总距离*/public abstract int calcDistance(int count);
}

在抽象类中提供一个抽象方法,也就是所有公园带实现的距离计算算法。这里因为是抽象类,所以就定义了abstract生命的抽象方法。下面便是针对此抽象类的具体实现,不同的公园有不同的算法实现。

人民公园的实现类PeopleParkStrategy。其中@Slf4j为Lombok的注解,可根据自己项目情况进行修改。

/*** 人民公园的实现**/
@Slf4j
public class PeopleParkStrategy extends AbstractParkStrategy {@Overridepublic int calcDistance(int count) {log.info("处理【人民公园】距离计算:count={}", count);// 默认 10米return 10 * count;}
}

北海公园的实现类BeiHaiParkStrategy:

/*** 北海公园的实现**/
@Slf4j
public class BeiHaiParkStrategy extends AbstractParkStrategy {@Overridepublic int calcDistance(int count) {log.info("处理【北海公园】距离计算:count={}",count);// 默认 10米return 50 * count;}
}

最后再提供一个默认的通用公园的实现类DefaultParkStrategy:

/*** 提供一个默认,通用的计算实现**/
@Slf4j
public class DefaultParkStrategy extends AbstractParkStrategy {@Overridepublic int calcDistance(int count) {log.info("处理【通用公园】距离计算:count={}", count);// 默认 20米return 20 * count;}
}

最后,我们来实现一个持有抽象类AbstractParkStrategy的环境角色类DistanceContext:

/*** 环境角色类**/
public class DistanceContext {/*** 持有策略抽象类*/private AbstractParkStrategy abstractParkStrategy;// 通过构造方法注入,也可以通过其他方式注入public DistanceContext(AbstractParkStrategy parkStrategy) {this.abstractParkStrategy = parkStrategy;}public int calcDistance(int count) {return abstractParkStrategy.calcDistance(count);}}

在该类中定义AbstractParkStrategy为其属性,通过构造方法传入实例化对象,并且提供了一个调用策略类的方法。

最后,我们来看一下如何调用该策略类,这里通过单元测试来完成:

@Slf4j
public class ParkTest {@Testpublic void testStrategy() {DistanceContext distanceContext = new DistanceContext(new PeopleParkStrategy());int distance = distanceContext.calcDistance(2);log.info("获得距离:{}", distance);}
}

单元测试模拟客户端的调用,当客户端实例化的是人民公园时,调用对应算法获得的便是人民公园的距离。如果创建的是其他公园的对象,则返回其他公园的距离。

策略模式的优缺点

从示例可以看出,策略模式仅仅封装算法,并不决定在何时使用何种算法。同时,在什么时候使用什么算法也是由客户端决定的。

同时策略模式有以下优缺点。优点:

  • 算法可以自由切换。
  • 使用策略模式可以避免使用多重条件(if-else)语句。多重条件语句不易维护,它把采取哪一种算法或采取哪一种行为的逻辑与算法或行为的逻辑混合在一起,统统列在一个多重条件语句里面,比使用继承的办法还要原始和落后。
  • 扩展性良好。增加新策略,只需实现接口的具体逻辑即可。当旧策略不需要时,直接剔除就行。
  • 良好的封装性。策略的入口封装在Context封装类中,客户端只要知道使用哪种策略对象就可以了。

缺点:

  • 客户端必须知道具体的策略,并且决定使用哪一个策略,也就是说各个策略需要暴露给客户端。
  • 如果策略增多,策略类的数量就会增加。对比上面的ifelse会发现增加了很多类。

策略工厂类改进

上面我们也看到了一个问题,就是客户端必须知道具体的策略实现类才能进行具体算法的调用。那么,是否可以将实现类进行封装,客户端只需传入对应的类型,然后直接调用方法就可以了?

此时,我们可以对策略模式的环境角色类进行改造,参考一下工厂模式的元素。修改之后的环境角色类变为FactoryContext:

public class FactoryContext {/*** 持有策略抽象类*/private AbstractParkStrategy parkStrategy;// 把创建策略放在封装角色内,客户端只需要知道结果public void factory(int strategyType) {if (strategyType == 1) {parkStrategy = new PeopleParkStrategy();} else if (strategyType == 2) {parkStrategy = new BeiHaiParkStrategy();} else {parkStrategy = new DefaultParkStrategy();}}public int calcDistance(int count) {return parkStrategy.calcDistance(count);}}

对应的单元测试类改为如下:

@Test
public void testFactoryStrategy() {FactoryContext factoryContext = new FactoryContext();factoryContext.factory(1);int distance = factoryContext.calcDistance(5);log.info("获得距离:{}", distance);
}

执行单元测试,发现可以正常输出结果。此时,客户端只用传入它自身的类型,不再关系具体类的创建了。

基于SpringBoot的策略模式

上面的改进已经更方便实践中的使用了,此时如果项目使用的是SpringBoot项目,那么还可以更进一步的封装和改进。请注意此步骤作为讲解验证的过度步骤,便于大家理解,此方式实现为非线程安全的,不建议在生产中使用。

此时主要利用Spring的@Autowired注解来将实例化的策略实现类注入到一个Map当中,然后通过key可以方便的拿到服务。这里使用concurrentHashMap是防止多线程操作的时候出现问题。

首先将策略实现类通过@Service注解进行实例化,并指定实例化的名称。以人民公园的实现为例:

@Slf4j
@Service("peopleParkStrategy")
public class PeopleParkStrategy extends AbstractParkStrategy {// ...
}

其他策略实现类与上相同,依次实例化。

最后改造环境角色类为DistanceSpringContext:

@Component("distanceSpringContext")
public class DistanceSpringContext {@Autowiredprivate final Map<String, AbstractParkStrategy> strategyMap = new ConcurrentHashMap<>(3);private AbstractParkStrategy strategy;public void factory(String serviceName) {strategy = strategyMap.get(serviceName);}public int calcDistance(int count) {return strategy.calcDistance(count);}
}

首先通过@Component注解将DistanceSpringContext的实例化也交由Spring来管理了。而@Autowired注解会将容器中AbstractParkStrategy的实现类(注解了@Service)注入到该Map中。其中key就是@Service中指定的实例化服务的名称,value值便是对应的对象。此时我们有三个策略实现类。因此,strategyMap中会被注入三个值。

既然其中有值存在,此时可通过serviceName来获得对应的服务,并调用相应的方法。

最后看一下单元测试的方法:

@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class ParkSpringTest {@Resourceprivate DistanceSpringContext distanceSpringContext;@Testpublic void testSpringStrategy() {distanceSpringContext.factory("defaultParkStrategy");int distance = distanceSpringContext.calcDistance(10);log.info("获得距离:{}", distance);}
}

直接在使用的地方注入DistanceSpringContext,然后根据具体的服务名来调用即可。不过,在最开始我们已经说过了,此种方法非线程安全,因为DistanceSpringContext是单例的,在factory方法设置的时候,其他线程也有可能修改。而且还需要传递Service的名称,使用起来还是不够方便。

进一步改进

针对SpringBoot集成的问题,我们再进一步改进。我们不再在策略角色类中调用策略类的方法了,只让策略角色类作为工厂的角色,返回对应的服务。而相关服务方法的调用由客户端直接调用实现类的方法。

同时,针对服务的名称和公园的Id我们通过枚举进行映射。先来定义一个枚举类:

public enum ParkEnum {DEFAULT_PARK(0, "defaultParkStrategy", "默认公园"),PERSON_PARK(1, "peopleParkStrategy", "人民公园"),BEI_HAI_PARK(2, "beiHaiParkStrategy", "北海公园"),;ParkEnum(int parkId, String serviceName, String desc) {this.parkId = parkId;this.serviceName = serviceName;this.desc = desc;}public static ParkEnum valueOf(int parkId) {for (ParkEnum parkEnum : ParkEnum.values()) {if (parkEnum.getParkId() == parkId) {return parkEnum;}}return DEFAULT_PARK;}private int parkId;private String serviceName;private String desc;public int getParkId() {return parkId;}public String getServiceName() {return serviceName;}public String getDesc() {return desc;}
}

在上述枚举类中,我们将公园的ID和对应的服务进行绑定,并提供了通过公园ID获得对应枚举的方法。

然后改造环境角色类:

@Component("distanceSpringV2Context")
public class DistanceSpringV2Context {@Autowiredprivate final Map<String, AbstractParkStrategy> strategyMap = new ConcurrentHashMap<>(3);public AbstractParkStrategy getService(int parkId) {ParkEnum parkEnum = ParkEnum.valueOf(parkId);return strategyMap.get(parkEnum.getServiceName());}}

通过枚举的转换,我们只需要接收公园ID这样一个参数便可以返回对应的具体策略实现类,在也中直接调用便可。而parkId每个公园都有,根本不需要进行业务判断。

下面看单元测试类:

@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class ParkSpringV2Test {@Resourceprivate DistanceSpringV2Context distanceSpringV2Context;@Testpublic void testSpringStrategy() {AbstractParkStrategy parkStrategy = distanceSpringV2Context.getService(1);int distance = parkStrategy.calcDistance(10);log.info("获得距离:{}", distance);}}

此时,只用传递对应的公园ID,然后获得对应的服务,然后直接调用服务的方法便可进行使用。是不是比最开始方便多了。此时,如果新添加算法,只用创建对应算法的服务,然后在枚举类中映射一下关系,便可在不影响客户端调用的情况进行扩展。当然,根据具体的业务场景还可以进行进一步的改造。

小结

最开始我们在讲策略模式,但演变到最后,发现它已经变得不那么像最初的策略模式了,并且有了工厂模式的身影。这也正是学习与实践的不同。单纯理论学习策略模式,是很难运用的好的。只有结合实践场景,根据需要进行改进。毕竟适合的最好的,就像,我们使用了SpringBoot便可根据SpringBoot的特性进行融合。回头看看你的项目,有没有类似的可改造的地方?行动起来吧。

相关源码,公众号“程序新视界”中回复“1001”即可获得链接。

原文链接:《SpringBoot下的策略模式,消灭了大量的ifelse,真香!》


程序新视界
公众号“程序新视界”,一个让你软实力、硬技术同步提升的平台,提供海量资料

SpringBoot下的策略模式,消灭了大量的ifelse,真香!相关推荐

  1. java spring 实现策略,Spring 环境下实现策略模式的示例

    背景 最近在忙一个需求,大致就是给满足特定条件的用户发营销邮件,但是用户的来源有很多方式:从 ES 查询的.从 csv 导入的.从 MongoDB 查询-.. 需求很简单,但是怎么写的优雅,方便后续扩 ...

  2. 设计模式:策略模式,避免冗长的if-else/switch分支判断代码

    策略模式的原理和实现 策略模式,英文全称是 Strategy Design Pattern.定义为:定义彝族算法类,将每个算法分别封装起来,让它们可以互相替换.策略模式可以使算法的变化独立于使用它们的 ...

  3. SpringBoot如何使用策略模式干掉if else

    场景:当我们接收到一些数据需要对其进行处理时,由于它们来自于不同的渠道(如:腾讯,头条),不同渠道所需的处理方式不同,下面我们写一个简单Demo来实现该的场景. 解决思路 实例一:简单举例 1.首先构 ...

  4. Springboot 使用设计模式- 策略模式

    前言 直白点,什么场景我们需要使用到设计模式- 策略模式. 在平常的springboot项目里面做CRUD,我们的习惯性基本是 一个mapper,一个service,一个serviceImpl. 但是 ...

  5. .NET 下运用策略模式

    我简单的理解策略模式就是把行为(方法)单独的抽象出来,并采用组合(Has-a)的方式,来组合行为和实体的一种模式.再来个官方的解释: Define a family of algorithms, en ...

  6. springboot服务使用策略模式

    1.前言 实际开发过程中,会进行大量的if else判断,这使得我们的代码非常臃肿且可读性较差.策略模式能够帮助我们很好的解决这个问题.下面以一个简单的例子说明如何在sprinboot项目中使用策略模 ...

  7. c++switch实现猜拳_策略模式+简单工厂+注解消除 if-else/switch-case

    消除代码中的 if-else/switch-case 在很多时候,我们代码中会有很多分支,而且分支下面的代码又有一些复杂的逻辑,相信很多人都喜欢用 if-else/switch-case 去实现.做的 ...

  8. if-else过多,使用策略模式(Strategy)解决if-else乱象

    if else 应用乱象主要分为以下几点: 1.if else 过多 2.if else 嵌套过深 3.表达式过于复杂 会导致以下的问题: 1.可读性差 2.可扩展性差 从软件设计角度来看, 不符合单 ...

  9. 基于SpringBoot 的CMS系统,拿去开发企业官网真香(附源码)

    前言 推荐这个项目是因为使用手册部署手册非常完善,项目也有开发教程视频对小白非常贴心,接私活可以直接拿去二开非常舒服 开源说明 系统100%开源 模块化开发模式,铭飞所开发的模块都发布到了maven中 ...

最新文章

  1. 【2】Vue项目引用Element UI(饿了么框架)菜单导航条初期配置
  2. 【网址收藏】Docker中ADD和COPY的区别
  3. 第三章 安装apache
  4. Cisco交换机实现端口安全与帮定
  5. WebApi Ajax 跨域请求解决方法(CORS实现)
  6. 使用layui弹框实现添加时,当添加成功之后如何进行关闭当前窗口刷新父页面的数据
  7. 数据库 数据库SQL语句五
  8. 窗体传值 父子窗体传值情况 c# 1231
  9. MAC系统上,软件安装后的目录
  10. 【运动学】基于matlab GUI模拟投篮系统(角度+力度可调)【含Matlab源码 1114期】
  11. java计算机毕业设计租车管理系统源码+mysql数据库+系统+部署+lw文档
  12. java流程图是什么形状,流程判断(流程图判断框什么形状)
  13. jsp post中文乱码
  14. 如何在Java中将Excel(XLSX)转换为Word(DOCX)
  15. [VC] 【Visual Studio】2005~2015中文完整旗舰版(附序列号)
  16. CS5212替代RTD2166|低BOM成本替代RTD2166
  17. 骑士游历问题【JAVA板】代码详细流
  18. 串口数据交换,实现串口合二为一
  19. java编译器对字符串+运算的优化导致的有趣现象
  20. 【RANSAC与单应性矩阵H求解】

热门文章

  1. h5拼手气红包java_Java模拟微信发红包(普通红包、拼手气红包)
  2. 2D横版跳跃游戏第三节
  3. 涨知识|最新十种深度学习算法要点及代码解析「精华」
  4. 阿里开源 EasyExcel(Excel导入导出 推荐使用)
  5. 中式红木装修,让豪宅更有韵味
  6. 计算机二级用英语简写,等级LV1,LV2是哪些英文的缩写?
  7. cocos creator shader实现汽车氮气加速特效
  8. Python实战|用可视化方式看新闻,迅速了解最新时事热点
  9. 复古磨砂纹理ps笔刷
  10. Gimbal Lock欧拉角死锁问题