• 从ifelse到策略模式,谈谈我对设计模式的理解


    前言

    一提到设计模式大家都会觉得很厉害,但是要用好设计模式确实不容易。甚至有很多人都不知道该在什么场景下使用设计模式。我之前就是这样,小傅哥的《重学Java设计模式》我也看了,但是看的时候好像看懂了,但是想在自己的项目中运用设计模式时,却不知道如何下手。不过最近在做一个项目时,通过大佬的一番指点,将策略模式运用到了项目之中。后来我仔细思考了一下,好像有点悟了,其实以前做过的很多项目中都可以运用到策略模式,而且使用策略模式后,代码的耦合度会降低扩展性也会增强。

    接下来我会结合一个具体的案例,从一开始的不用设计模式,一步步地优化代码,来聊一聊该如何使用策略模式。

    从ifelse到策略模式的进化

    假如现在有这样一个活动。随机给用户抽取十道题目,如果用户答对其中6道题,就可以获得一份礼品。

    这个功能的实现包括抽取题目、判断用户回答正确的题目数、发放礼品等多个环节。现在只针对判断用户回答正确的题目数这一个环节进行讲解。

    Step1:一撸到底

    现在的需求还是比较简单的,就是循环比对用户的答案与数据库中的答案。直接开撸即可,不需要任何花哨的技巧也可以轻松的完成。

    1. @Service
    2. public class ActivityServiceImpl implements ActivityService {
    3. @Override
    4. public Result<Boolean> submitAnswers(AnswersSubmitReq req) {
    5. if (Objects.isNull(req.getAnswers()) || req.getAnswers().size() < 10) {
    6. return Result.<Boolean>fail().message("提交的答案数量有误");
    7. }
    8. int rightAnswerCount = 0;
    9. for (AnswerReq answer : req.getAnswers()) {
    10. // 根据题目id获取从数据库中获取正确答案。此步骤略。假定正确答案是A
    11. String right = "A";
    12. rightAnswerCount += StrUtil.equals(right, answer.getUserAnswer()) ? 1 : 0;
    13. }
    14. if (rightAnswerCount >= 6) {
    15. return Result.<Boolean>success().data(true).message("闯关成功");
    16. }
    17. return Result.<Boolean>success().data(false).message("闯关失败");
    18. }
    19. }
    20. 复制代码

    代码很简单,也很好的实现了功能。如果需求没有进行变更,当然没有问题,但要是需求改变了,代码也要随之更改。

    Step2:if...else...

    当程序员开发完成后,运维以及产品经理在一起研究讨论发现。现在的活动规则过于简单,少了一些趣味性。为了适当的增加一些趣味性以及挑战性,将整个答题活动分为了三个关卡,关卡由易到难分别为简单、中等、困难,三个关卡都通过才能获得礼品。题目也设为了三个等级:简单、中等、困难

    • 简单模式:10道简单题。答对其中6道即算过关。
    • 中等模式:5道简单题,3道中等题,2道困难题。答对3道简单题,2道中等题,1道困难题即算过关。
    • 困难模式:6道中等题,4道困难题。答对4道中等题,2道困难题即算过关。
    1. @Service
    2. public class ActivityServiceImpl implements ActivityService {
    3. @Override
    4. public Result<Boolean> submitAnswers(AnswersSubmitReq req, Integer level) {
    5. if (Objects.isNull(req.getAnswers()) || req.getAnswers().size() < 10) {
    6. return Result.<Boolean>fail().message("提交的答案数量有误");
    7. }
    8. if (level == 1) {
    9. // 答案的判定。省略…………
    10. } else if (level == 2) {
    11. // 答案的判定。省略…………
    12. } else if (level == 3) {
    13. // 答案的判定。省略…………
    14. }
    15. return Result.<Boolean>success().data(true);
    16. }
    17. }
    18. 复制代码

    这个时候,需求的复杂度已经提升了一个等级。虽然从实现上来说也没有什么难度,不过是答案的判断而已。但是当代码写完后会发现,里面有一大坨的if...else...。如果后续需求再次发生变化或者有bug。去定位需要修改的位置也要耗费一定的时间,代码的可维护性就会降低。如果后续再推出第4关,第5关,那么将会有更多的if...else...,所以这种方式也不具备良好的扩展性。最关键的是,这种方式写出来的代码将会很难看,对于一个追求代码整齐、清晰的人来说,简直不能够容忍。

    Step3:使用策略模式优化代码

    我们先来看一下策略模式的定义:

    指定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。

    从定义上来看,好像策略模式用起来很不错的样子,那我们就来具体实现一下:

    首先在入参的 AnswersSubmitReq 中添加一个字段,用于标识将要采取哪一个策略

    1. @Data
    2. public class AnswersSubmitReq {
    3. …………
    4. /**
    5. * 答题策略
    6. */
    7. @NotNull
    8. private Integer answerMode;
    9. }
    10. 复制代码

    然后再去新建一个策略接口,所有的策略实现都去实现这个接口👇。

    1. public interface ICommitAnswer {
    2. Result<Boolean> execute(List<AnswerReq> param);
    3. }
    4. 复制代码

    👆这个就是策略的接口,策略的实现类都去实现这个接口然后实现其中的execute方法。

    1. public class EasyCommitAnswer implements ICommitAnswer {
    2. @Override
    3. public Result<Boolean> execute(List<AnswerReq> param) {
    4. int rightAnswer = 0; // 正确回答的数量
    5. for (AnswerReq answer : param) {
    6. // 根据题目id获取从数据库中获取正确答案。此步骤略。假定正确答案是A
    7. String right = "A";
    8. rightAnswer += StrUtil.equals(answer.getUserAnswer(), right) ? 1 : 0;
    9. }
    10. if (rightAnswer >= 6) {
    11. return Result.<Boolean>success().data(true);
    12. }
    13. return Result.<Boolean>fail().data(false);
    14. }
    15. }
    16. ---------------------------------------------------------------------------------------
    17. public class MediumCommitAnswer implements ICommitAnswer {
    18. @Override
    19. public Result<Boolean> execute(List<AnswerReq> param) {
    20. Map<Integer, Integer> rightAnswer = new HashMap<>();
    21. rightAnswer.put(AnswerStrategyEnum.EASY.getCode(), 0); // 简单题回答正确的数量
    22. rightAnswer.put(AnswerStrategyEnum.MEDIUM.getCode(), 0); // 中等题回答正确的数量
    23. rightAnswer.put(AnswerStrategyEnum.HARD.getCode(), 0); // 困难题回答正确的数量
    24. for (AnswerReq answer : param) {
    25. // 根据题目id获取从数据库中获取正确答案。此步骤略。假定正确答案是A
    26. String right = "A";
    27. int addCount = StrUtil.equals(right, answer.getUserAnswer()) ? 1 : 0;
    28. rightAnswer.put(answer.getLevel(), rightAnswer.get(answer.getLevel()) + addCount);
    29. }
    30. if (rightAnswer.get(AnswerStrategyEnum.EASY.getCode()) >= 3
    31. && rightAnswer.get(AnswerStrategyEnum.MEDIUM.getCode()) >= 2
    32. && rightAnswer.get(AnswerStrategyEnum.HARD.getCode()) >= 1) {
    33. return Result.<Boolean>success().data(true).message("闯关成功");
    34. }
    35. return Result.<Boolean>fail().data(false).message("闯关失败");
    36. }
    37. }
    38. ---------------------------------------------------------------------------------------
    39. public class HardCommitAnswer implements ICommitAnswer {
    40. @Override
    41. public Result<Boolean> execute(List<AnswerReq> param) {
    42. Map<Integer, Integer> rightAnswer = new HashMap<>();
    43. rightAnswer.put(AnswerStrategyEnum.MEDIUM.getCode(), 0); // 中等题回答正确的数量
    44. rightAnswer.put(AnswerStrategyEnum.HARD.getCode(), 0); // 困难题回答正确的数量
    45. for (AnswerReq answer : param) {
    46. // 根据题目id获取从数据库中获取正确答案。此步骤略。假定正确答案是A
    47. String right = "A";
    48. int addCount = StrUtil.equals(right, answer.getUserAnswer()) ? 1 : 0;
    49. rightAnswer.put(answer.getLevel(), rightAnswer.get(answer.getLevel()) + addCount);
    50. }
    51. if (rightAnswer.get(AnswerStrategyEnum.EASY.getCode()) >= 3
    52. && rightAnswer.get(AnswerStrategyEnum.MEDIUM.getCode()) >= 2
    53. && rightAnswer.get(AnswerStrategyEnum.HARD.getCode()) >= 1) {
    54. return Result.<Boolean>success().data(true).message("闯关成功");
    55. }
    56. return Result.<Boolean>fail().data(false).message("闯关失败");
    57. }
    58. @Override
    59. public Result<Boolean> execute(Map<Integer, Integer> rightAnswerCountMap) {
    60. if (rightAnswerCountMap.get(AnswerStrategyEnum.EASY.getCode()) >= 3
    61. && rightAnswerCountMap.get(AnswerStrategyEnum.MEDIUM.getCode()) >= 2
    62. && rightAnswerCountMap.get(AnswerStrategyEnum.HARD.getCode()) >= 1) {
    63. return Result.<Boolean>success().data(true).message("闯关成功");
    64. }
    65. return Result.<Boolean>success().data(false).message("闯关失败");
    66. }
    67. }
    68. 复制代码

    现在我们只需要根据不同的场景去调用不同的策略就可以了:

    1. @Service
    2. public class ActivityServiceImpl implements ActivityService {
    3. @Override
    4. public Result<Boolean> submitAnswers(AnswersSubmitReq req) {
    5. if (Objects.isNull(req.getAnswers()) || req.getAnswers().size() < 10) {
    6. return Result.<Boolean>fail().message("提交的答案数量有误");
    7. }
    8. List<AnswerReq> answers = req.getAnswers();
    9. if (req.getAnswerMode() == 1) {
    10. ICommitAnswer answerStrategy = new EasyCommitAnswer();
    11. return answerStrategy.execute(answers);
    12. } else if (req.getAnswerMode() == 2) {
    13. ICommitAnswer answerStrategy = new MediumCommitAnswer();
    14. return answerStrategy.execute(answers);
    15. } else if (req.getAnswerMode() == 3) {
    16. ICommitAnswer answerStrategy = new HardCommitAnswer();
    17. return answerStrategy.execute(answers);
    18. }
    19. }
    20. }
    21. 复制代码

    策略模式到这里就差不多完成了。具体的策略都由不同的策略实现类决定,与调用方无关,Service层的代码看起来也整齐多了。如果后续某个策略要进行修改,那么去修改对应的策略就好,调用方不需要修改。如果要增加新的策略,那么Service层也只需要进行简单的调整就可以。可维护性与扩展性都大大地得到了提升。

    Step4:策略模式再优化

    看样子上面的代码好像没有什么问题了,但是Service层在调用策略的时候,不还是要通过if...else...来进行判断吗。只不过是从代码流程的切换变为了对策略调用的判断。

    其实这也是可以解决的。首先我们要知道一点,就是外部肯定是知道它自己是要调用哪个策略的,所以我们只需要给每个策略编一个号,外部调用时传个编号过来(上一节的answerMode字段)。我们通过一个Map将所有的策略都装起来,编号就作为Map的key,那通过key不就可以取到对应的策略类了嘛。

    1. public class CommitAnswerFactory {
    2. private static final Map<Integer, ICommitAnswer> answerStrategies = new HashMap<>();
    3. static {
    4. answerStrategies.put(AnswerStrategyEnum.EASY.getCode(), new EasyCommitAnswer());
    5. answerStrategies.put(AnswerStrategyEnum.MEDIUM.getCode(), new MediumCommitAnswer());
    6. answerStrategies.put(AnswerStrategyEnum.HARD.getCode(), new HardCommitAnswer());
    7. }
    8. public static ICommitAnswer getAnswerStrategy(Integer mode) {
    9. return answerStrategies.get(mode);
    10. }
    11. }
    12. 复制代码

    在策略的工厂类中,通过一个Map将策略的对象放入其中,然后提供一个getAnswerStrategy方法,只要将策略的编号传入,就可以从Map中取出对应的策略实现类了。这样Service层在调用时就不需要使用if...else...进行判断了👇

    1. @Service
    2. public class ActivityServiceImpl implements ActivityService {
    3. @Override
    4. public Result<Boolean> submitAnswers(AnswersSubmitReq req) {
    5. if (Objects.isNull(req.getAnswers()) || req.getAnswers().size() < 10) {
    6. return Result.<Boolean>fail().message("提交的答案数量有误");
    7. }
    8. List<AnswerReq> answers = req.getAnswers();
    9. ICommitAnswer answerStrategy = CommitAnswerFactory.getAnswerStrategy(req.getAnswerMode());
    10. return answerStrategy.execute(rightAnswerCountMap);
    11. }
    12. }
    13. 复制代码

    Step5:进一步抽取公共代码,简化代码

    不知道大家有没有发现,三个策略中好像都有一段很相似的代码,就是对于正确答案的判断。仔细分析三个策略就可以发现,其实三个策略中不同的地方仅仅是在于对结果的判断,而统计不同难度答对题目的数量操作都是相同的,都是循环比对用户答案与数据库中的答案是否一致,然后进行计数。

    既然有公共的地方就可以提取出来,那么提取到哪里比较合适呢?既然三个策略都实现了ICommitAnswer接口,那么不如就将公共代码放入ICommitAnswer接口中去。

    1. public interface ICommitAnswer {
    2. Result<Boolean> execute(List<AnswerReq> param);
    3. default Map<Integer,Integer> computerRightCount(List<AnswerReq> param) {
    4. Map<Integer, Integer> rightAnswerCountMap = new HashMap<>();
    5. rightAnswerCountMap.put(AnswerStrategyEnum.EASY.getCode(), 0); // 简单题回答正确的数量
    6. rightAnswerCountMap.put(AnswerStrategyEnum.MEDIUM.getCode(), 0); // 中等题回答正确的数量
    7. rightAnswerCountMap.put(AnswerStrategyEnum.HARD.getCode(), 0); // 困难题回答正确的数量
    8. for (AnswerReq answer : param) {
    9. // 根据题目id获取从数据库中获取正确答案。此步骤略。假定正确答案是A
    10. String right = "A";
    11. int addCount = StrUtil.equals(right, answer.getUserAnswer()) ? 1 : 0;
    12. rightAnswerCountMap.put(answer.getLevel(),
    13. rightAnswerCountMap.get(answer.getLevel()) + addCount);
    14. }
    15. return rightAnswerCountMap;
    16. }
    17. }
    18. 复制代码

    现在在接口中添加了computerRightCount方法,并为其添加了默认实现,这个方法就是计算各个难度的题目分别答对了多少题。然后将结果放入一个Map集合中。

    1. @Service
    2. public class ActivityServiceImpl implements ActivityService {
    3. @Override
    4. public Result<Boolean> submitAnswers(AnswersSubmitReq req) {
    5. if (Objects.isNull(req.getAnswers()) || req.getAnswers().size() < 10) {
    6. return Result.<Boolean>fail().message("提交的答案数量有误");
    7. }
    8. List<AnswerReq> answers = req.getAnswers();
    9. ICommitAnswer answerStrategy = CommitAnswerFactory.getAnswerStrategy(req.getAnswerMode());
    10. return answerStrategy.execute(answers);
    11. }
    12. }
    13. 复制代码

    这样在具体的策略中只需要对正确答案的数量进行判断即可。

    1. public class EasyCommitAnswer implements ICommitAnswer {
    2. @Override
    3. public Result<Boolean> execute(List<AnswerReq> answers) {
    4. Map<Integer, Integer> rightAnswerCountMap = this.computerRightCount(answers);
    5. if (rightAnswerCountMap.get(AnswerStrategyEnum.EASY.getCode()) >= 6) {
    6. return Result.<Boolean>success().data(true).message("闯关成功");
    7. }
    8. return Result.<Boolean>success().data(false).message("闯关失败");
    9. }
    10. }
    11. ---------------------------------------------------------------------------------------
    12. public class MediumCommitAnswer implements ICommitAnswer {
    13. @Override
    14. public Result<Boolean> execute(List<AnswerReq> answers) {
    15. Map<Integer, Integer> rightAnswerCountMap = this.computerRightCount(answers);
    16. if (rightAnswerCountMap.get(AnswerStrategyEnum.EASY.getCode()) >= 3
    17. && rightAnswerCountMap.get(AnswerStrategyEnum.MEDIUM.getCode()) >= 2
    18. && rightAnswerCountMap.get(AnswerStrategyEnum.HARD.getCode()) >= 1) {
    19. return Result.<Boolean>success().data(true).message("闯关成功");
    20. }
    21. return Result.<Boolean>success().data(false).message("闯关失败");
    22. }
    23. }
    24. ---------------------------------------------------------------------------------------
    25. public class HardCommitAnswer implements ICommitAnswer {
    26. @Override
    27. public Result<Boolean> execute(List<AnswerReq> answers) {
    28. Map<Integer, Integer> rightAnswerCountMap = this.computerRightCount(answers);
    29. if (rightAnswerCountMap.get(AnswerStrategyEnum.EASY.getCode()) >= 3
    30. && rightAnswerCountMap.get(AnswerStrategyEnum.MEDIUM.getCode()) >= 2
    31. && rightAnswerCountMap.get(AnswerStrategyEnum.HARD.getCode()) >= 1) {
    32. return Result.<Boolean>success().data(true).message("闯关成功");
    33. }
    34. return Result.<Boolean>success().data(false).message("闯关失败");
    35. }
    36. }
    37. 复制代码

    上述的案例中,并不只有这一个地方可以使用策略模式。抽取题目不也分为几种情况吗,那么这不也可以使用策略模式进行包装吗😄

    策略模式实现业务之间的解耦

    其实策略模式不仅可以实现上述这种不同业务流程之间的切换,也可以实现不同业务之间的解耦。比如我最近在做的一个项目,我需要对表中的某个字段进行更新,但是更新却分为了几种情况,这几种情况分别散落在不同的业务中。这其实是一件非常恶心的事,因为一个业务中竟然掺杂了对其它业务的处理,如果日后是别人接手了我的代码,那么他看到某个业务中出现了这样一段代码肯定会一脸懵逼。这个地方为什么要对这个字段进行更新?到底还有哪些地方对这个字段进行了操作?所以可维护性就很差。不说别人,可能过段时间过后,我自己都忘了为什么要这么写了。

    其实我一开始并没有意识到这个问题,但是通过大佬的一番指点,我采用了策略模式去实现,将对该字段的操作封装成几个策略,然后在不同的业务场景下调用不同的策略。因为一个策略我只调用了一次,所以通过查看这几个策略分别在哪些地方被调用了,我就可以知道有哪些地方对这个字段进行了操作。优点就是代码更加清晰了,维护起来也方便了,同时也避免了多个业务之间的耦合。

    总结

    其实使用一个设计模式并不一定要完全照搬,因为使用设计模式的目的还是为了代码的整洁、可维护性与可扩展性。所以在使用的时候可以按照自己的使用场景做适当的调整。最重要的还是要理解不同的设计模式到底解决了什么问题,适用于什么场景。当你感觉一段代码写完后看起来感觉比较恶心的时候,就应该思考,是不是可以使用某种设计模式去优化代码。

    以上就是我这段时间在项目中使用过策略模式后的一些思考与总结,因为用的不多,所以很多东西说的可能有些片面或者不太正确。有问题欢迎在评论区留言讨论!

  • 相关阅读:
    面试:linux相关
    2021论文阅读笔记集合
    解决uniapp里scroll-view横向滚动的问题
    java-python-php音乐分享网站播放器系统vue+elementui
    shell中的条件控制语句
    矩阵的乘法运算与css的3d变换(transform)
    Linux文件系统——文件系统、挂载点、目录结构
    关于机器学习的向量机,都讲了什么
    计算机毕业设计springboot+vue基本微信小程序的学生健康管理小程序 uniapp
    JavaWeb-解析Http协议
  • 原文地址:https://blog.csdn.net/BASK2312/article/details/127665466