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

·  阅读 4683

我正在参加「掘金·启航计划」

前言

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

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

从ifelse到策略模式的进化

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

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

Step1:一撸到底

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

@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("闯关失败");
    }

}
复制代码

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

Step2:if...else...

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

  • 简单模式: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...,所以这种方式也不具备良好的扩展性。最关键的是,这种方式写出来的代码将会很难看,对于一个追求代码整齐、清晰的人来说,简直不能够容忍。

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

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

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

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

首先在入参的 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层也只需要进行简单的调整就可以。可维护性与扩展性都大大地得到了提升。

Step4:策略模式再优化

看样子上面的代码好像没有什么问题了,但是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);
    }
}
复制代码

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

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

既然有公共的地方就可以提取出来,那么提取到哪里比较合适呢?既然三个策略都实现了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("闯关失败");
    }
}
复制代码

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

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

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

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

总结

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

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

代码:github.com/RobodLee/St…

本文已收录至我的Github仓库DayDayUPgithub.com/RobodLee/Da…,欢迎Star

如果您觉得文章还不错,请给我来个点赞收藏关注

学习更多编程知识,欢迎关注微信关注公众号『 R o b o d 』:

分类:
后端
收藏成功!
已添加到「」, 点击更改