在上一讲中,我们学习了模板方法模式。模板方法模式能帮助我们进行公有方法的抽取,起到快速复用和扩展的作用。今天,我们接着学习另外一种可以快速复用和扩展代码的行为型模式:策略模式。
策略模式在实际的开发中很常用,最常见的应用场景是利用它来替换过多的 if-else 嵌套的逻辑判断。除此之外,它还能结合工厂模式给客户端提供非常灵活的使用体验。
那么,话不多说,让我们开始今天的课程吧。
模式原理分析
策略模式的原始定义是:定义一系列算法,封装每个算法,并使它们可以互换。策略让算法独立于使用它的客户端而变化。
在这个定义中,策略模式明确表示应当由客户端自己决定在什么样的情况下使用哪些具体的策略。也就是说,服务端是作为一个策略的整体调控者,具体选择运行哪些策略其实是要交给客户端来决定的。比如,压缩文件的时候,你应该会提供一系列的不同压缩策略,比如,gzip、zip 等,至于客户端在什么时候使用 gzip,由客户端自行去决定。同时,gzip 还可以被替换为其他的压缩策略。
我们来看看策略模式 UML 图的结构:
从该 UML 图中,我们能看出策略模式包含了三个关键角色。
-
上下文信息类(Context):用于存放和执行需要使用的具体策略类以及客户端调用的逻辑。
-
抽象策略类(Strategy):定义策略的共有方法。
-
具体策略类(StrategyA 等):实现抽象策略类定义的共有方法。
下面我们再来看看 UML 对应的代码实现:
public class Context {
public void request(Strategy s) {
s.operation();
}
}
public interface Strategy {
void operation();
}
public class StrategyA implements Strategy {
@Override
public void operation() {
System.out.println("=== 执行策略 A ......");
}
}
public class StrategyB implements Strategy {
@Override
public void operation() {
System.out.println("=== 执行策略 B ......");
}
}
从上面的代码实现我们能看出,策略模式的本质就是通过上下文信息类来作为中心控制单元,对不同的策略进行调度分配。
使用场景分析
策略模式常见的使用场景有以下几种。
-
系统中需要动态切换几种算法的场景。
-
使用多重的条件选择语句来实现的业务场景。
-
只希望客户端选择已经封装好的算法场景而不用关心算法实现细节。
-
分离使用策略和创建策略的场景。
为了帮助你更好地理解策略模式的使用场景,下面我们还是通过一个简单的例子来演示说明。在日常的网上购物中,我们都希望购买到价格实惠的商品,而网站通常也会定期搞一些促销活动,比如,满 XXX 元减 XX 元、N 折扣、M 元秒杀等。这些活动通常是运营部门来制定的,运营希望能通过商品编号来推荐不同的促销活动。这时,作为系统的开发者,我们就需要开发针对一系列的营销策略进行推荐的系统功能。
那具体该怎么来实现呢?
首先,我们来定义策略 API——PromotionStrategy,每一种促销策略的算法都要实现该接口。该接口有一个 recommand 方法,接收并返回一个 int 对象,返回的就是推荐后可以参加的促销活动。实际上,推荐返回的可能是一个活动对象,这里我用简单的数字代替。
public interface PromotionStrategy {
/**
* 返回1 代表 可以参加 满减活动
* 返回2 代表 可以参加 N折优惠活动
* 返回3 代表 可以参加 M元秒杀活动
*/
int recommand(String skuId);
}
接下来,我们有三个类实现了 PromotionStrategy 接口,分别代表满减策略、N 折扣优惠活动策略和 M 元秒杀活动策略,类分别是 FullReduceStrategy、NPriceDiscountStrategy 和 MSpikeStrategy。
public class FullReduceStrategy implements PromotionStrategy {
@Override
public int recommand(String skuId) {
System.out.println("=== 执行 满减活动");
//推荐算法和逻辑写这里
return 1;
}
}
public class NPriceDiscountStrategy implements PromotionStrategy {
@Override
public int recommand(String skuId) {
System.out.println("=== 执行 N 折扣优惠活动");
//推荐算法和逻辑写这里
return 2;
}
}
public class MSpikeStrategy implements PromotionStrategy {
@Override
public int recommand(String skuId) {
System.out.println("=== 执行 M 元秒杀活动");
//推荐算法和逻辑写这里
return 3;
}
}
现在我们可以动手实现促销推荐的上下文信息类 Promotional,这里是存储和使用策略的地方。类中有一个 recommand 方法,用于执行推荐策略。它的构造函数有一个 PromotionStrategy 参数,可以在运行期间使用该参数决定使用哪种促销策略。
public class Promotional {
private final PromotionStrategy strategy;
public Promotional(PromotionStrategy strategy) {
this.strategy = strategy;
}
public void recommand(String skuId) {
strategy.recommand(skuId);
}
}
最后,我们通过一个简单的单元测试代码来看下具体的运行结果。
public class Client {
public static void main(String[] args) {
Promotional fullReducePromotional = new Promotional(new FullReduceStrategy());
fullReducePromotional.recommand("1122334455");
Promotional nPriceDiscountPromotional = new Promotional(new NPriceDiscountStrategy());
nPriceDiscountPromotional.recommand("6677889900");
Promotional mSpikePromotional = new Promotional(new MSpikeStrategy());
mSpikePromotional.recommand("11335577");
}
}
//输出结果
=== 执行 满减活动
=== 执行 n折扣优惠活动
=== 执行 m元秒杀活动
以上代码演示了一个使用策略模式的基本流程,在实际的开发中,策略类可能会包含复杂的逻辑,这里就不再展开详细讲述了。
为什么使用策略模式?
分析完策略模式的原理和使用场景后,我们再来说说使用策略模式的原因,主要有以下三个。
第一个,为了提升代码的可维护性。 在实际开发中,有许多算法可以实现某一功能,如查找、排序等,通过 if-else 等条件判断语句来进行选择非常方便。但是这就会带来一个问题:当在这个算法类中封装了大量查找算法时,该类的代码就会变得非常复杂,维护也会突然就变得非常困难。虽然策略模式看上去比较笨重,但实际上在每一次新增策略时都通过新增类来进行隔离,短期虽然不如直接写 if-else 来得效率高,但长期来看,维护单一的简单类耗费的时间其实远远低于维护一个超大的复杂类。
第二个,为了动态快速地替换更多的算法。 从上面的分析你会发现,策略模式最大的作用在于分离使用算法的逻辑和算法自身实现的逻辑,这样就意味着当我们想要优化算法自身的实现逻辑时就变得非常便捷,一方面可以采用最新的算法实现逻辑,另一方面可以直接弃用旧算法而采用新算法。使用策略模式能够很方便地进行替换。
第三个,为了应对需要频繁更换策略的场景。 比如,用户推荐类场景。特别是对于一些 C 端产品来说,在获取了用户的反馈数据后,会根据用户的特性制定不同的运营策略,这时如果采用 if-else 的方式编码,那么每一次的策略变化都会导致系统代码的修改,从运营的角度看是不可接受的,而采用策略模式就能很容易地解决这个问题。
收益什么?损失什么?
通过上面的分析,我们可以得出使用策略模式主要有以下几个优点。
-
提供良好的代码扩展性。 每一个策略都是对应生成一个新的具体策略类,满足开闭原则,同时满足里氏替换原则,可以任意替换相同的策略,这样用户可以在不修改原有系统的基础上选择算法或行为,同时也可以灵活地增加新的算法或行为。
-
提供了一种管理多个不同算法策略的办法。 策略模式提供了一种很好的思路,可以将算法的实现和使用算法的代码隔离开来,这样就能很好地管理不同的算法。
-
提供使用组合替换继承的办法。 策略模式使用组合的方式来替代继承,避免了子类出现异常而影响父类。
-
能够降低使用多重条件(if-else)嵌套语句的理解难度。 在实际的开发中,使用 if-else 是非常常见的编程方法,但是随着业务逻辑变得越来越复杂,如果一味地增加 if-else,会让代码变得非常难以理解和维护,使用策略模式则能避免这些问题的出现。
-
在运行时动态切换算法,提升代码灵活性。 由于策略模式将算法的选择权交给了客户端,那么客户端可以根据自身的需求灵活地切换算法。
同样,策略模式也不是万能的,它也有一些缺点。
-
客户端的学习成本变高。 虽然策略模式让客户端自行决定使用哪一个策略,看上去很自由,但实际上隐含着客户必须要知道所有的策略才能做选择的事实。一旦新增或修改策略,客户端都需要知道。
-
具体策略类的数量会剧增,增加维护成本。 由于每一个策略都对应一个具体策略类,所以当策略比较庞大时,需要维护的类数量也会激增。
-
不如函数式编程简洁。 现在有很多编程语言都支持函数式——允许在一组匿名函数中实现不同版本的算法。对于一些小型的策略来说,使用函数式编程就能解决问题,但使用策略模式反而过于复杂。
总结
策略模式最大的用处是能在运行时改变代码的算法行为,同时给使用者提供一种可以根据情况来选择算法的途径。
虽然策略模式是一个比较容易理解和使用的设计模式,但是却增加了使用者的难度,因为你可能需要在了解了所有的策略后才能做出决策。即便是类似排序这样简单的算法,不同使用者的选择也可能完全不同,如果交给使用者来选择,就意味着使用者需要了解不同排序算法的优劣,才能更好地做出选择。
不过,策略模式对算法起到了很好的封装作用,通过使用算法和创建算法的分离,将算法实现的复杂性放到了子类去解决。同时,策略模式还可以随时进行替换,对于一些老旧的算法,可以很方便地进行替换和升级。
课后思考
在你所参与过的项目中,都是怎样使用策略模式的?有没有替换策略模式的其他办法?欢迎你在留言区与我分享。
在下一讲,我会接着与你分享“状态模式”的相关内容,记得按时来听课!