每个人都应该会的设计模式-策略模式详解

前言

策略模式是最常用的设计模式之一,尤其是在消除if-else语句时,总是能够看到策略模式的身影。它将一组行为转化为对象,使其能够在原始上下文对象内部能够相互替换

问题

一天,你为游客们创建一款导游程序。程序的首个版本能够支持规划驾车路线,驾车旅行的人使用它非常的满意。随着程序的流行,下个版本中你为程序添加了步行规划的功能。此时程序也只有两种路线程序写起来非常简单。但再下个版本中添加了骑行规划的功能,再随后增加了公交路线规划,又过了一段时间,你又要为浏览城市中的所有景点规划路线。

从商业的角度来看,你的程序随着流行度的增长变得非常成功。但是从技术的角度来看,每次添加新的路线规划算法后,程序中的主要类体积就要增加一倍。

终于在未来的某个时候,你没有办法维护这个程序了。无论是简单的修复BUG还是微调算法中对于某个街道的权重,对某个算法进行任何的修改都会影响到整个类。从而增加在原本正常的代码中引入错误的风险。团队合作也变得低效,没有人愿意维护一段冗长的代码。即使是团队中的新成员也会抱怨在合并代码冲突上话费太多的时间。

解决方案

策略模式中建议找出负责用不同方式完成特定任务的类,然后将其中的算法抽取到被称为策略的独立类中。

名为上下文的原始类包含一个成员变量来存储对每种策略的引用。原始类并不执行任务,而是将任务委派给已连接的策略对象。

上下文不负责选择哪种算法是符合任务需要的 -- 客户端会将所需策略传递给上下文。实际上,上下文对策略是不关心的。只是通过同样的通用接口与所有策略进行交互。这个通用接口只需要暴露一个方法来触发所选策略中封装的方法即可。

因此,上下文独立于策略。这样就可以在不修改上下文代码或其他策略的情况下增加新算法或者修改已有的算法了。

对于下图的策略模式结构

结构

image-20211231235926706

  1. 上下文 (Context) 维护指向具体策略的引用, 且仅通过策略接口与该对象进行交流。
  2. 策略 (Strategy) 接口是所有具体策略的通用接口, 它声明了一个上下文用于执行策略的方法。
  3. 具体策略 (Concrete Strategies) 实现了上下文所用算法的各种不同变体。
  4. 当上下文需要运行算法时, 它会在其已连接的策略对象上调用执行方法。 上下文不清楚其所涉及的策略类型与算法的执行方式。
  5. 客户端 (Client) 会创建一个特定策略对象并将其传递给上下文。 上下文则会提供一个设置器以便客户端在运行时替换相关联的策略。

实现步骤

  1. 从上下文类(原始类)中找出修改频率较高的算法(也可能是用于在运行时选择某个算法变体的复杂条件运算符)
  2. 声明算法所有变体的通用策略接口
  3. 将算法抽取到各自类中,实现通用策略接口。
  4. 在上下文类中添加一个成员变量用于保存对策略对象的引用。提供设置器以能够修改该成员变量(策略)。上下仅通过策略接口同策略对象进行交互。
  5. 客户端将上下文与相应策略进行关联,使上下文可以按预期方式完成工作。

适用场景

当有许多仅在执行某些行为时略有不同的相似类时

如果算法在上下文的逻辑中不是特别重要,希望将上下文与算法实现细节隔离开来。

当类中使用了复杂条件运算符在同一算法中的不同变体中切换时。

核心Java程序库中策略模式的示例

  1. java.util.Comparator#compare()的调用来自Collctions#sort()
  2. javax.servlet.http.HttpServlet#service()方法
  3. javax.servlet.Filter#doFilter()

实战场景

策略模式尤其适用于电子商务应用中实现各种支付方法。客户在某个支付场景时需要选择一种支付方式:微信支付或AliPay。

同时,我们以不同邮件服务供应商的邮件发送为例:

为了消除if-else语句,在Spring应用中,我们可以这样来进行消除

public interface MailStrategyService {
​
    /**
     * 服务提供商名称
     *
     * @return
     */
    public String getServiceProviderName();
​
    /**
     * 发送邮件
     *
     * @param message
     */
    public void send(String message);
​
}
复制代码
@Service
@Slf4j
public class QQMailStrategyServiceImpl implements MailStrategyService {
​
    @Override
    public String getServiceProviderName() {
        return "qq";
    }
​
    @Override
    public void send(String message) {
       log.info("向QQ邮箱发送邮件:{}",message);
    }
}
复制代码

核心需要学习的地方:

@Component
@Slf4j
@Getter
public class MailStrategyContext {
​
    /**
     * 策略
     * KEY为业务编码
     * VALUE为具体实现类
     */
    private final ConcurrentHashMap<String, MailStrategyService> strategy = new ConcurrentHashMap<>();
​
    /**
     * 注入所有实现 MailStrategyService 接口的类
     * 这里使用的是构造注入的方式将Bean注入进来
     *
     * @param mailServiceList
     */
    public MailStrategyContext(List<MailStrategyService> mailServiceList) {
        log.info("开始注入策略");
        mailServiceList.forEach(mailServiceImpl -> {
            log.info("当前策略类:{}", mailServiceImpl.getClass().getName());
            strategy.put(mailServiceImpl.getServiceProviderName(), mailServiceImpl);
        });
        log.info("注入策略完毕");
    }
​
    /**
     * 发送邮件
     *
     * @param strategyName 策略名称
     * @param message      发送邮件信息
     */
    public void send(String strategyName, String message) {
        MailStrategyService mailStrategyService = strategy.get(strategyName);
        mailStrategyService.send(message);
    }
}
复制代码
@RestController
@RequestMapping("/mail")
public class StrategyController {
    @Autowired
    private MailStrategyContext mailStrategy;
​
    @PostMapping("/{bussinessCode}/send")
    public void sendMail(@PathVariable(value = "bussinessCode") String bussinessCode,
                         @RequestParam(value = "message") String message) {
        mailStrategy.send(bussinessCode, message);
    }
}
复制代码

强烈推荐一个在线设计模式的学习网站:refactoringguru.cn/design-patt…

猜你喜欢

转载自juejin.im/post/7047903844876943374