【设计模式】策略模式

27 阅读7分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第18天,点击查看活动详情

【设计模式】策略模式

【设计模式】策略模式一:策略模式简介二:策略模式使用场景三:策略模式总结四:策略模式实战

一:策略模式简介

在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。

在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对象的执行算法。

二:策略模式使用场景

结合策略模式的概念,我们找一个实际的场景来理解一下。

假设我们是一家新开的书店,为了招揽顾客,我们推出会员服务,我们把店里的会员分为三种,分别是初级会员、中级会员和高级会员。针对不同级别的会员我们给予不同的优惠。初级会员买书我们不打折、中级会员买书我们打九折、高级会员买书我们打八折。

在不使用模式的情况下,我们可以在结算的方法中使用if/else语句来区别出不同的会员来计算价格。

但是,如果我们有一天想要把初级会员的折扣改成9.8折怎么办?有一天我要推出超级会员怎么办?有一天我要针对中级会员可打折的书的数量做限制怎么办?

使用if\else设计出来的系统,所有的算法都写在了一起,只要有改动我就要修改整个类。我们都知道,只要是修改代码就有可能引入问题。为了避免这个问题,我们可以使用策略模式。。。

策略模式包含如下角色:

  • Context: 环境类
  • Strategy: 抽象策略类
  • ConcreteStrategy: 具体策略类

image-20221215134726848

先定义一个接口,这个接口就是抽象策略类,该接口定义了计算价格方法,具体实现方式由具体的策略类来定义。

public interface Member {
​
    /**
     * 计算应付价格
     * @param bookPrice 书籍原价(针对金额,建议使用BigDecimal,double会损失精度)
     * @return 应付金额
     */
    public double calPrice(double bookPrice);
}

针对不同的会员,定义三种具体的策略类,每个类中都分别实现计算价格方法。

/**
 * 初级会员
 */
public class PrimaryMember implements Member {
​
    @Override
    public double calPrice(double bookPrice) {
        System.out.println("对于初级会员的没有折扣");
        return bookPrice;
    }
}
​
​
/**
 * 中级会员,买书打九折
 */
public class IntermediateMember implements Member {
​
    @Override
    public double calPrice(double bookPrice) {
        System.out.println("对于中级会员的折扣为10%");
        return bookPrice * 0.9;
    }
}
​
​
/**
 * 高级会员,买书打八折
 */
public class AdvancedMember implements Member {
​
    @Override
    public double calPrice(double bookPrice) {
        System.out.println("对于中级会员的折扣为20%");
        return bookPrice * 0.8;
    }
}

上面几个类的定义体现了封装变化的设计原则,不同会员的具体折扣方式改变不会影响到其他的会员。

定义好了抽象策略类和具体策略类之后,我们再来定义环境类,所谓环境类,就是集成算法的类。这个例子中就是收银台系统。采用组合的方式把会员集成进来。

/**
 * 书籍价格类
 */
public class Cashier {
​
    /**
     * 会员,策略对象
     */
    private Member member;
​
    public Cashier(Member member){
        this.member = member;
    }
​
    /**
     * 计算应付价格
     * @param booksPrice
     * @return
     */
    public double quote(double booksPrice) {
        return this.member.calPrice(booksPrice);
    }
}

这个Cashier类就是一个环境类,该类的定义体现了多用组合,少用继承针对接口编程,不针对实现编程两个设计原则。由于这里采用了组合+接口的方式,后面我们在推出超级会员的时候无须修改Cashier类。只要再定义一个SuperMember implements Member 就可以了。

下面定义一个客户端来测试一下:

public class BookStore {
​
    public static void main(String[] args) {
​
        //选择并创建需要使用的策略对象
        Member strategy = new AdvancedMember();
        //创建环境
        Cashier cashier = new Cashier(strategy);
        //计算价格
        double quote = cashier.quote(300);
        System.out.println("高级会员图书的最终价格为:" + quote);
​
        strategy = new IntermediateMember();
        cashier = new Cashier(strategy);
        quote = cashier.quote(300);
        System.out.println("中级会员图书的最终价格为:" + quote);
    }
}
​
//对于中级会员的折扣为20%
//高级会员图书的最终价格为:240.0
//对于中级会员的折扣为10%
//中级会员图书的最终价格为:270.0

三:策略模式总结

  • 如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
  • 一个系统需要动态地在几种算法中选择一种。
  • 如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。

四:策略模式实战

我想小伙伴们经常有这样的不满,我的业务逻辑就3 4 行,你给我整一大堆类定义?有必要这么麻烦吗?简单点行不行。

其实我们所不满的就是策略模式带来的缺点

  • 策略类会增多
  • 业务逻辑分散到各个实现类中,而且没有一个地方可以俯视整个业务逻辑

针对传统策略模式的缺点,在这分享一个实现思路,这个思路可以解决了多个复杂if else的业务场景,理解上比较容易,代码上需要用到Java8的特性——利用Map与函数式接口来实现。

其中:

  • getCheckResult() 为传统的做法
  • getCheckResultSuper()则事先在Map中定义好了“判断条件”与“业务逻辑”的映射关系,具体讲解请看代码注释
/**
 * 某个业务服务类
 */
@Service
public class BizService {
​
    /**
     * 传统的 if else 解决方法
     * 当每个业务逻辑有 3 4 行时,用传统的策略模式不值得,直接的if else又显得不易读
     */
    public String getCheckResult(String order) {
        if ("校验1".equals(order)) {
            return "执行业务逻辑1";
        } else if ("校验2".equals(order)) {
            return "执行业务逻辑2";
        }else if ("校验3".equals(order)) {
            return "执行业务逻辑3";
        }else if ("校验4".equals(order)) {
            return "执行业务逻辑4";
        }else if ("校验5".equals(order)) {
            return "执行业务逻辑5";
        }else if ("校验6".equals(order)) {
            return "执行业务逻辑6";
        }else if ("校验7".equals(order)) {
            return "执行业务逻辑7";
        }else if ("校验8".equals(order)) {
            return "执行业务逻辑8";
        }else if ("校验9".equals(order)) {
            return "执行业务逻辑9";
        }
        return "不在处理的逻辑中返回业务错误";
    }
​
    /**
     * 业务逻辑分派Map
     * Function为函数式接口,下面代码中 Function<String, String> 的含义是接收一个Stirng类型的变量,返回一个String类型的结果
     */
    private Map<String, Function<String, String>> checkResultDispatcher = new HashMap<>();
​
    /**
     * 初始化 业务逻辑分派Map 其中value 存放的是 lambda表达式
     */
    @PostConstruct
    public void checkResultDispatcherInit() {
        checkResultDispatcher.put("校验1", order -> String.format("对%s执行业务逻辑1", order));
        checkResultDispatcher.put("校验2", order -> String.format("对%s执行业务逻辑2", order));
        checkResultDispatcher.put("校验3", order -> String.format("对%s执行业务逻辑3", order));
        checkResultDispatcher.put("校验4", order -> String.format("对%s执行业务逻辑4", order));
        checkResultDispatcher.put("校验5", order -> String.format("对%s执行业务逻辑5", order));
        checkResultDispatcher.put("校验6", order -> String.format("对%s执行业务逻辑6", order));
        checkResultDispatcher.put("校验7", order -> String.format("对%s执行业务逻辑7", order));
        checkResultDispatcher.put("校验8", order -> String.format("对%s执行业务逻辑8", order));
        checkResultDispatcher.put("校验9", order -> String.format("对%s执行业务逻辑9", order));
    }
​
    public String getCheckResultSuper(String order) {
        //从逻辑分派Dispatcher中获得业务逻辑代码,result变量是一段lambda表达式
        Function<String, String> result = checkResultDispatcher.get(order);
        if (result != null) {
            //执行这段表达式获得String类型的结果
            return result.apply(order);
        }
        return "不在处理的逻辑中返回业务错误";
    }
}

通过http调用一下看看效果:

/**
 * 模拟一次http调用
 */
@RestController
public class BizController {
​
    @Autowired
    private BizService bizService;
​
    @PostMapping("/v1/biz/testSuper")
    public String test2(String order) {
        return bizService.getCheckResultSuper(order);
    }
}

image-20221215135618593

好处很直观:

  • 在一段代码里直观的看到”判断条件”与业务逻辑的映射关系
  • 不需要单独定义接口与实现类,直接使用现有的函数式接口(什么?不知道函数式接口?快去了解),而实现类直接就是业务代码本身。