本文已收录至我的Github仓库DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star
一提到设计模式大家都会觉得很厉害,但是要用好设计模式确实不容易。甚至有很多人都不知道该在什么场景下使用设计模式。我之前就是这样,小傅哥的《重学Java设计模式》我也看了,但是看的时候好像看懂了,但是想在自己的项目中运用设计模式时,却不知道如何下手。不过最近在做一个项目时,通过大佬的一番指点,将策略模式运用到了项目之中。后来我仔细思考了一下,好像有点悟了,其实以前做过的很多项目中都可以运用到策略模式,而且使用策略模式后,代码的耦合度会降低扩展性也会增强。
接下来我会结合一个具体的案例,从一开始的不用设计模式,一步步地优化代码,来聊一聊该如何使用策略模式。
假如现在有这样一个活动。随机给用户抽取十道题目,如果用户答对其中6道题,就可以获得一份礼品。
这个功能的实现包括抽取题目、判断用户回答正确的题目数、发放礼品等多个环节。现在只针对判断用户回答正确的题目数这一个环节进行讲解。
现在的需求还是比较简单的,就是循环比对用户的答案与数据库中的答案。直接开撸即可,不需要任何花哨的技巧也可以轻松的完成。
@Service
public class ActivityServiceImpl implements ActivityService {
@Override
public Result<Boolean> submitAnswers(AnswersSubmitReq req) {
if (Objects.isNull(req.getAnswers()) || req.getAnswers().size() < 10) {
return Result.<Boolean>fail().message("提交的答案数量有误");
}
int rightAnswerCount = 0;
for (AnswerReq answer : req.getAnswers()) {
// 根据题目id获取从数据库中获取正确答案。此步骤略。假定正确答案是A
String right = "A";
rightAnswerCount += StrUtil.equals(right, answer.getUserAnswer()) ? 1 : 0;
}
if (rightAnswerCount >= 6) {
return Result.<Boolean>success().data(true).message("闯关成功");
}
return Result.<Boolean>success().data(false).message("闯关失败");
}
}
代码很简单,也很好的实现了功能。如果需求没有进行变更,当然没有问题,但要是需求改变了,代码也要随之更改。
当程序员开发完成后,运维以及产品经理在一起研究讨论发现。现在的活动规则过于简单,少了一些趣味性。为了适当的增加一些趣味性以及挑战性,将整个答题活动分为了三个关卡,关卡由易到难分别为简单、中等、困难,三个关卡都通过才能获得礼品。题目也设为了三个等级:简单、中等、困难。
简单模式
:10道简单题。答对其中6道即算过关。中等模式
:5道简单题,3道中等题,2道困难题。答对3道简单题,2道中等题,1道困难题即算过关。困难模式
:6道中等题,4道困难题。答对4道中等题,2道困难题即算过关。@Service
public class ActivityServiceImpl implements ActivityService {
@Override
public Result<Boolean> submitAnswers(AnswersSubmitReq req, Integer level) {
if (Objects.isNull(req.getAnswers()) || req.getAnswers().size() < 10) {
return Result.<Boolean>fail().message("提交的答案数量有误");
}
if (level == 1) {
// 答案的判定。省略…………
} else if (level == 2) {
// 答案的判定。省略…………
} else if (level == 3) {
// 答案的判定。省略…………
}
return Result.<Boolean>success().data(true);
}
}
这个时候,需求的复杂度已经提升了一个等级。虽然从实现上来说也没有什么难度,不过是答案的判断而已。但是当代码写完后会发现,里面有一大坨的if…else…。如果后续需求再次发生变化或者有bug。去定位需要修改的位置也要耗费一定的时间,代码的可维护性就会降低。如果后续再推出第4关,第5关,那么将会有更多的if…else…,所以这种方式也不具备良好的扩展性。最关键的是,这种方式写出来的代码将会很难看,对于一个追求代码整齐、清晰的人来说,简直不能够容忍。
我们先来看一下策略模式的定义:
指定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。
从定义上来看,好像策略模式用起来很不错的样子,那我们就来具体实现一下:
首先在入参的 AnswersSubmitReq 中添加一个字段,用于标识将要采取哪一个策略
@Data
public class AnswersSubmitReq {
…………
/**
* 答题策略
*/
@NotNull
private Integer answerMode;
}
然后再去新建一个策略接口,所有的策略实现都去实现这个接口。
public interface ICommitAnswer {
Result<Boolean> execute(List<AnswerReq> param);
}
这个就是策略的接口,策略的实现类都去实现这个接口然后实现其中的execute
方法。
public class EasyCommitAnswer implements ICommitAnswer {
@Override
public Result<Boolean> execute(List<AnswerReq> param) {
int rightAnswer = 0; // 正确回答的数量
for (AnswerReq answer : param) {
// 根据题目id获取从数据库中获取正确答案。此步骤略。假定正确答案是A
String right = "A";
rightAnswer += StrUtil.equals(answer.getUserAnswer(), right) ? 1 : 0;
}
if (rightAnswer >= 6) {
return Result.<Boolean>success().data(true);
}
return Result.<Boolean>fail().data(false);
}
}
---------------------------------------------------------------------------------------
public class MediumCommitAnswer implements ICommitAnswer {
@Override
public Result<Boolean> execute(List<AnswerReq> param) {
Map<Integer, Integer> rightAnswer = new HashMap<>();
rightAnswer.put(AnswerStrategyEnum.EASY.getCode(), 0); // 简单题回答正确的数量
rightAnswer.put(AnswerStrategyEnum.MEDIUM.getCode(), 0); // 中等题回答正确的数量
rightAnswer.put(AnswerStrategyEnum.HARD.getCode(), 0); // 困难题回答正确的数量
for (AnswerReq answer : param) {
// 根据题目id获取从数据库中获取正确答案。此步骤略。假定正确答案是A
String right = "A";
int addCount = StrUtil.equals(right, answer.getUserAnswer()) ? 1 : 0;
rightAnswer.put(answer.getLevel(), rightAnswer.get(answer.getLevel()) + addCount);
}
if (rightAnswer.get(AnswerStrategyEnum.EASY.getCode()) >= 3
&& rightAnswer.get(AnswerStrategyEnum.MEDIUM.getCode()) >= 2
&& rightAnswer.get(AnswerStrategyEnum.HARD.getCode()) >= 1) {
return Result.<Boolean>success().data(true).message("闯关成功");
}
return Result.<Boolean>fail().data(false).message("闯关失败");
}
}
---------------------------------------------------------------------------------------
public class HardCommitAnswer implements ICommitAnswer {
@Override
public Result<Boolean> execute(List<AnswerReq> param) {
Map<Integer, Integer> rightAnswer = new HashMap<>();
rightAnswer.put(AnswerStrategyEnum.MEDIUM.getCode(), 0); // 中等题回答正确的数量
rightAnswer.put(AnswerStrategyEnum.HARD.getCode(), 0); // 困难题回答正确的数量
for (AnswerReq answer : param) {
// 根据题目id获取从数据库中获取正确答案。此步骤略。假定正确答案是A
String right = "A";
int addCount = StrUtil.equals(right, answer.getUserAnswer()) ? 1 : 0;
rightAnswer.put(answer.getLevel(), rightAnswer.get(answer.getLevel()) + addCount);
}
if (rightAnswer.get(AnswerStrategyEnum.EASY.getCode()) >= 3
&& rightAnswer.get(AnswerStrategyEnum.MEDIUM.getCode()) >= 2
&& rightAnswer.get(AnswerStrategyEnum.HARD.getCode()) >= 1) {
return Result.<Boolean>success().data(true).message("闯关成功");
}
return Result.<Boolean>fail().data(false).message("闯关失败");
}
@Override
public Result<Boolean> execute(Map<Integer, Integer> rightAnswerCountMap) {
if (rightAnswerCountMap.get(AnswerStrategyEnum.EASY.getCode()) >= 3
&& rightAnswerCountMap.get(AnswerStrategyEnum.MEDIUM.getCode()) >= 2
&& rightAnswerCountMap.get(AnswerStrategyEnum.HARD.getCode()) >= 1) {
return Result.<Boolean>success().data(true).message("闯关成功");
}
return Result.<Boolean>success().data(false).message("闯关失败");
}
}
现在我们只需要根据不同的场景去调用不同的策略就可以了:
@Service
public class ActivityServiceImpl implements ActivityService {
@Override
public Result<Boolean> submitAnswers(AnswersSubmitReq req) {
if (Objects.isNull(req.getAnswers()) || req.getAnswers().size() < 10) {
return Result.<Boolean>fail().message("提交的答案数量有误");
}
List<AnswerReq> answers = req.getAnswers();
if (req.getAnswerMode() == 1) {
ICommitAnswer answerStrategy = new EasyCommitAnswer();
return answerStrategy.execute(answers);
} else if (req.getAnswerMode() == 2) {
ICommitAnswer answerStrategy = new MediumCommitAnswer();
return answerStrategy.execute(answers);
} else if (req.getAnswerMode() == 3) {
ICommitAnswer answerStrategy = new HardCommitAnswer();
return answerStrategy.execute(answers);
}
}
}
策略模式到这里就差不多完成了。具体的策略都由不同的策略实现类决定,与调用方无关,Service层的代码看起来也整齐多了。如果后续某个策略要进行修改,那么去修改对应的策略就好,调用方不需要修改。如果要增加新的策略,那么Service层也只需要进行简单的调整就可以。可维护性与扩展性都大大地得到了提升。
看样子上面的代码好像没有什么问题了,但是Service层在调用策略的时候,不还是要通过if…else…来进行判断吗。只不过是从代码流程的切换变为了对策略调用的判断。
其实这也是可以解决的。首先我们要知道一点,就是外部肯定是知道它自己是要调用哪个策略的,所以我们只需要给每个策略编一个号,外部调用时传个编号过来(上一节的answerMode字段)。我们通过一个Map将所有的策略都装起来,编号就作为Map的key,那通过key不就可以取到对应的策略类了嘛。
public class CommitAnswerFactory {
private static final Map<Integer, ICommitAnswer> answerStrategies = new HashMap<>();
static {
answerStrategies.put(AnswerStrategyEnum.EASY.getCode(), new EasyCommitAnswer());
answerStrategies.put(AnswerStrategyEnum.MEDIUM.getCode(), new MediumCommitAnswer());
answerStrategies.put(AnswerStrategyEnum.HARD.getCode(), new HardCommitAnswer());
}
public static ICommitAnswer getAnswerStrategy(Integer mode) {
return answerStrategies.get(mode);
}
}
在策略的工厂类中,通过一个Map将策略的对象放入其中,然后提供一个getAnswerStrategy
方法,只要将策略的编号传入,就可以从Map中取出对应的策略实现类了。这样Service层在调用时就不需要使用if…else…进行判断了
@Service
public class ActivityServiceImpl implements ActivityService {
@Override
public Result<Boolean> submitAnswers(AnswersSubmitReq req) {
if (Objects.isNull(req.getAnswers()) || req.getAnswers().size() < 10) {
return Result.<Boolean>fail().message("提交的答案数量有误");
}
List<AnswerReq> answers = req.getAnswers();
ICommitAnswer answerStrategy = CommitAnswerFactory.getAnswerStrategy(req.getAnswerMode());
return answerStrategy.execute(rightAnswerCountMap);
}
}
不知道大家有没有发现,三个策略中好像都有一段很相似的代码,就是对于正确答案的判断。仔细分析三个策略就可以发现,其实三个策略中不同的地方仅仅是在于对结果的判断,而统计不同难度答对题目的数量操作都是相同的,都是循环比对用户答案与数据库中的答案是否一致,然后进行计数。
既然有公共的地方就可以提取出来,那么提取到哪里比较合适呢?既然三个策略都实现了ICommitAnswer
接口,那么不如就将公共代码放入ICommitAnswer接口中去。
public interface ICommitAnswer {
Result<Boolean> execute(List<AnswerReq> param);
default Map<Integer,Integer> computerRightCount(List<AnswerReq> param) {
Map<Integer, Integer> rightAnswerCountMap = new HashMap<>();
rightAnswerCountMap.put(AnswerStrategyEnum.EASY.getCode(), 0); // 简单题回答正确的数量
rightAnswerCountMap.put(AnswerStrategyEnum.MEDIUM.getCode(), 0); // 中等题回答正确的数量
rightAnswerCountMap.put(AnswerStrategyEnum.HARD.getCode(), 0); // 困难题回答正确的数量
for (AnswerReq answer : param) {
// 根据题目id获取从数据库中获取正确答案。此步骤略。假定正确答案是A
String right = "A";
int addCount = StrUtil.equals(right, answer.getUserAnswer()) ? 1 : 0;
rightAnswerCountMap.put(answer.getLevel(),
rightAnswerCountMap.get(answer.getLevel()) + addCount);
}
return rightAnswerCountMap;
}
}
现在在接口中添加了computerRightCount方法,并为其添加了默认实现,这个方法就是计算各个难度的题目分别答对了多少题。然后将结果放入一个Map集合中。
@Service
public class ActivityServiceImpl implements ActivityService {
@Override
public Result<Boolean> submitAnswers(AnswersSubmitReq req) {
if (Objects.isNull(req.getAnswers()) || req.getAnswers().size() < 10) {
return Result.<Boolean>fail().message("提交的答案数量有误");
}
List<AnswerReq> answers = req.getAnswers();
ICommitAnswer answerStrategy = CommitAnswerFactory.getAnswerStrategy(req.getAnswerMode());
return answerStrategy.execute(answers);
}
}
这样在具体的策略中只需要对正确答案的数量进行判断即可。
public class EasyCommitAnswer implements ICommitAnswer {
@Override
public Result<Boolean> execute(List<AnswerReq> answers) {
Map<Integer, Integer> rightAnswerCountMap = this.computerRightCount(answers);
if (rightAnswerCountMap.get(AnswerStrategyEnum.EASY.getCode()) >= 6) {
return Result.<Boolean>success().data(true).message("闯关成功");
}
return Result.<Boolean>success().data(false).message("闯关失败");
}
}
---------------------------------------------------------------------------------------
public class MediumCommitAnswer implements ICommitAnswer {
@Override
public Result<Boolean> execute(List<AnswerReq> answers) {
Map<Integer, Integer> rightAnswerCountMap = this.computerRightCount(answers);
if (rightAnswerCountMap.get(AnswerStrategyEnum.EASY.getCode()) >= 3
&& rightAnswerCountMap.get(AnswerStrategyEnum.MEDIUM.getCode()) >= 2
&& rightAnswerCountMap.get(AnswerStrategyEnum.HARD.getCode()) >= 1) {
return Result.<Boolean>success().data(true).message("闯关成功");
}
return Result.<Boolean>success().data(false).message("闯关失败");
}
}
---------------------------------------------------------------------------------------
public class HardCommitAnswer implements ICommitAnswer {
@Override
public Result<Boolean> execute(List<AnswerReq> answers) {
Map<Integer, Integer> rightAnswerCountMap = this.computerRightCount(answers);
if (rightAnswerCountMap.get(AnswerStrategyEnum.EASY.getCode()) >= 3
&& rightAnswerCountMap.get(AnswerStrategyEnum.MEDIUM.getCode()) >= 2
&& rightAnswerCountMap.get(AnswerStrategyEnum.HARD.getCode()) >= 1) {
return Result.<Boolean>success().data(true).message("闯关成功");
}
return Result.<Boolean>success().data(false).message("闯关失败");
}
}
上述的案例中,并不只有这一个地方可以使用策略模式。抽取题目不也分为几种情况吗,那么这不也可以使用策略模式进行包装吗
其实策略模式不仅可以实现上述这种不同业务流程之间的切换,也可以实现不同业务之间的解耦。比如我最近在做的一个项目,我需要对表中的某个字段进行更新,但是更新却分为了几种情况,这几种情况分别散落在不同的业务中。这其实是一件非常恶心的事,因为一个业务中竟然掺杂了对其它业务的处理,如果日后是别人接手了我的代码,那么他看到某个业务中出现了这样一段代码肯定会一脸懵逼。这个地方为什么要对这个字段进行更新?到底还有哪些地方对这个字段进行了操作?所以可维护性就很差。不说别人,可能过段时间过后,我自己都忘了为什么要这么写了。
其实我一开始并没有意识到这个问题,但是通过大佬的一番指点,我采用了策略模式去实现,将对该字段的操作封装成几个策略,然后在不同的业务场景下调用不同的策略。因为一个策略我只调用了一次,所以通过查看这几个策略分别在哪些地方被调用了,我就可以知道有哪些地方对这个字段进行了操作。优点就是代码更加清晰了,维护起来也方便了,同时也避免了多个业务之间的耦合。
其实使用一个设计模式并不一定要完全照搬,因为使用设计模式的目的还是为了代码的整洁、可维护性与可扩展性。所以在使用的时候可以按照自己的使用场景做适当的调整。最重要的还是要理解不同的设计模式到底解决了什么问题,适用于什么场景。当你感觉一段代码写完后看起来感觉比较恶心的时候,就应该思考,是不是可以使用某种设计模式去优化代码。
以上就是我这段时间在项目中使用过策略模式后的一些思考与总结,因为用的不多,所以很多东西说的可能有些片面或者不太正确。有问题欢迎在评论区留言讨论!
代码:https://github.com/RobodLee/StrategyPatternDemo
本文已收录至我的Github仓库DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star
如果您觉得文章还不错,请给我来个
点赞
,收藏
,关注