CategoryResourceRepost/极客时间专栏/设计模式之美/设计模式与范式:行为型/60 | 策略模式(上):如何避免冗长的if-else|switch分支判断代码?.md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

248 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<audio id="audio" title="60 | 策略模式如何避免冗长的if-else/switch分支判断代码" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ea/bc/ea222c6642f138d498bb397f9a0cc1bc.mp3"></audio>
上两节课中,我们学习了模板模式。模板模式主要起到代码复用和扩展的作用。除此之外,我们还讲到了回调,它跟模板模式的作用类似,但使用起来更加灵活。它们之间的主要区别在于代码实现,模板模式基于继承来实现,回调基于组合来实现。
今天我们开始学习另外一种行为型模式策略模式。在实际的项目开发中这个模式也比较常用。最常见的应用场景是利用它来避免冗长的if-else或switch分支判断。不过它的作用还不止如此。它也可以像模板模式那样提供框架的扩展点等等。
对于策略模式,我们分两节课来讲解。今天,我们讲解策略模式的原理和实现,以及如何用它来避免分支判断逻辑。下一节课,我会通过一个具体的例子,来详细讲解策略模式的应用场景以及真正的设计意图。
话不多说,让我们正式开始今天的学习吧!
## 策略模式的原理与实现
策略模式英文全称是Strategy Design Pattern。在GoF的《设计模式》一书中它是这样定义的
>
Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
翻译成中文就是:定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的客户端(这里的客户端代指使用算法的代码)。
我们知道,工厂模式是解耦对象的创建和使用,观察者模式是解耦观察者和被观察者。策略模式跟两者类似,也能起到解耦的作用,不过,它解耦的是策略的定义、创建、使用这三部分。接下来,我就详细讲讲一个完整的策略模式应该包含的这三个部分。
### 1.策略的定义
策略类的定义比较简单,包含一个策略接口和一组实现这个接口的策略类。因为所有的策略类都实现相同的接口,所以,客户端代码基于接口而非实现编程,可以灵活地替换不同的策略。示例代码如下所示:
```
public interface Strategy {
void algorithmInterface();
}
public class ConcreteStrategyA implements Strategy {
@Override
public void algorithmInterface() {
//具体的算法...
}
}
public class ConcreteStrategyB implements Strategy {
@Override
public void algorithmInterface() {
//具体的算法...
}
}
```
### 2.策略的创建
因为策略模式会包含一组策略在使用它们的时候一般会通过类型type来判断创建哪个策略来使用。为了封装创建逻辑我们需要对客户端代码屏蔽创建细节。我们可以把根据type创建策略的逻辑抽离出来放到工厂类中。示例代码如下所示
```
public class StrategyFactory {
private static final Map&lt;String, Strategy&gt; strategies = new HashMap&lt;&gt;();
static {
strategies.put(&quot;A&quot;, new ConcreteStrategyA());
strategies.put(&quot;B&quot;, new ConcreteStrategyB());
}
public static Strategy getStrategy(String type) {
if (type == null || type.isEmpty()) {
throw new IllegalArgumentException(&quot;type should not be empty.&quot;);
}
return strategies.get(type);
}
}
```
一般来讲如果策略类是无状态的不包含成员变量只是纯粹的算法实现这样的策略对象是可以被共享使用的不需要在每次调用getStrategy()的时候,都创建一个新的策略对象。针对这种情况,我们可以使用上面这种工厂类的实现方式,事先创建好每个策略对象,缓存到工厂类中,用的时候直接返回。
相反,如果策略类是有状态的,根据业务场景的需要,我们希望每次从工厂方法中,获得的都是新创建的策略对象,而不是缓存好可共享的策略对象,那我们就需要按照如下方式来实现策略工厂类。
```
public class StrategyFactory {
public static Strategy getStrategy(String type) {
if (type == null || type.isEmpty()) {
throw new IllegalArgumentException(&quot;type should not be empty.&quot;);
}
if (type.equals(&quot;A&quot;)) {
return new ConcreteStrategyA();
} else if (type.equals(&quot;B&quot;)) {
return new ConcreteStrategyB();
}
return null;
}
}
```
### 3.策略的使用
刚刚讲了策略的定义和创建,现在,我们再来看一下,策略的使用。
我们知道,策略模式包含一组可选策略,客户端代码一般如何确定使用哪个策略呢?最常见的是运行时动态确定使用哪种策略,这也是策略模式最典型的应用场景。
这里的“运行时动态”指的是,我们事先并不知道会使用哪个策略,而是在程序运行期间,根据配置、用户输入、计算结果等这些不确定因素,动态决定使用哪种策略。接下来,我们通过一个例子来解释一下。
```
// 策略接口EvictionStrategy
// 策略类LruEvictionStrategy、FifoEvictionStrategy、LfuEvictionStrategy...
// 策略工厂EvictionStrategyFactory
public class UserCache {
private Map&lt;String, User&gt; cacheData = new HashMap&lt;&gt;();
private EvictionStrategy eviction;
public UserCache(EvictionStrategy eviction) {
this.eviction = eviction;
}
//...
}
// 运行时动态确定,根据配置文件的配置决定使用哪种策略
public class Application {
public static void main(String[] args) throws Exception {
EvictionStrategy evictionStrategy = null;
Properties props = new Properties();
props.load(new FileInputStream(&quot;./config.properties&quot;));
String type = props.getProperty(&quot;eviction_type&quot;);
evictionStrategy = EvictionStrategyFactory.getEvictionStrategy(type);
UserCache userCache = new UserCache(evictionStrategy);
//...
}
}
// 非运行时动态确定,在代码中指定使用哪种策略
public class Application {
public static void main(String[] args) {
//...
EvictionStrategy evictionStrategy = new LruEvictionStrategy();
UserCache userCache = new UserCache(evictionStrategy);
//...
}
}
```
从上面的代码中我们也可以看出“非运行时动态确定”也就是第二个Application中的使用方式并不能发挥策略模式的优势。在这种应用场景下策略模式实际上退化成了“面向对象的多态特性”或“基于接口而非实现编程原则”。
## 如何利用策略模式避免分支判断?
实际上,能够移除分支判断逻辑的模式不仅仅有策略模式,后面我们要讲的状态模式也可以。对于使用哪种模式,具体还要看应用场景来定。 策略模式适用于根据不同类型的动态,决定使用哪种策略这样一种应用场景。
我们先通过一个例子来看下if-else或switch-case分支判断逻辑是如何产生的。具体的代码如下所示。在这个例子中我们没有使用策略模式而是将策略的定义、创建、使用直接耦合在一起。
```
public class OrderService {
public double discount(Order order) {
double discount = 0.0;
OrderType type = order.getType();
if (type.equals(OrderType.NORMAL)) { // 普通订单
//...省略折扣计算算法代码
} else if (type.equals(OrderType.GROUPON)) { // 团购订单
//...省略折扣计算算法代码
} else if (type.equals(OrderType.PROMOTION)) { // 促销订单
//...省略折扣计算算法代码
}
return discount;
}
}
```
如何来移除掉分支判断逻辑呢?那策略模式就派上用场了。我们使用策略模式对上面的代码重构,将不同类型订单的打折策略设计成策略类,并由工厂类来负责创建策略对象。具体的代码如下所示:
```
// 策略的定义
public interface DiscountStrategy {
double calDiscount(Order order);
}
// 省略NormalDiscountStrategy、GrouponDiscountStrategy、PromotionDiscountStrategy类代码...
// 策略的创建
public class DiscountStrategyFactory {
private static final Map&lt;OrderType, DiscountStrategy&gt; strategies = new HashMap&lt;&gt;();
static {
strategies.put(OrderType.NORMAL, new NormalDiscountStrategy());
strategies.put(OrderType.GROUPON, new GrouponDiscountStrategy());
strategies.put(OrderType.PROMOTION, new PromotionDiscountStrategy());
}
public static DiscountStrategy getDiscountStrategy(OrderType type) {
return strategies.get(type);
}
}
// 策略的使用
public class OrderService {
public double discount(Order order) {
OrderType type = order.getType();
DiscountStrategy discountStrategy = DiscountStrategyFactory.getDiscountStrategy(type);
return discountStrategy.calDiscount(order);
}
}
```
重构之后的代码就没有了if-else分支判断语句了。实际上这得益于策略工厂类。在工厂类中我们用Map来缓存策略根据type直接从Map中获取对应的策略从而避免if-else分支判断逻辑。等后面讲到使用状态模式来避免分支判断逻辑的时候你会发现它们使用的是同样的套路。本质上都是借助“查表法”根据type查表代码中的strategies就是表替代根据type分支判断。
但是,如果业务场景需要每次都创建不同的策略对象,我们就要用另外一种工厂类的实现方式了。具体的代码如下所示:
```
public class DiscountStrategyFactory {
public static DiscountStrategy getDiscountStrategy(OrderType type) {
if (type == null) {
throw new IllegalArgumentException(&quot;Type should not be null.&quot;);
}
if (type.equals(OrderType.NORMAL)) {
return new NormalDiscountStrategy();
} else if (type.equals(OrderType.GROUPON)) {
return new GrouponDiscountStrategy();
} else if (type.equals(OrderType.PROMOTION)) {
return new PromotionDiscountStrategy();
}
return null;
}
}
```
这种实现方式相当于把原来的if-else分支逻辑从OrderService类中转移到了工厂类中实际上并没有真正将它移除。关于这个问题如何解决我今天先暂时卖个关子。你可以在留言区说说你的想法我在下一节课中再讲解。
## 重点回顾
好了,今天的内容到此就讲完了。我们一块来总结回顾一下,你需要重点掌握的内容。
策略模式定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的客户端(这里的客户端代指使用算法的代码)。
策略模式用来解耦策略的定义、创建、使用。实际上,一个完整的策略模式就是由这三个部分组成的。
- 策略类的定义比较简单,包含一个策略接口和一组实现这个接口的策略类。
- 策略的创建由工厂类来完成,封装策略创建的细节。
- 策略模式包含一组策略可选,客户端代码如何选择使用哪个策略,有两种确定方法:编译时静态确定和运行时动态确定。其中,“运行时动态确定”才是策略模式最典型的应用场景。
除此之外我们还可以通过策略模式来移除if-else分支判断。实际上这得益于策略工厂类更本质上点讲是借助“查表法”根据type查表替代根据type分支判断。
## 课堂讨论
今天我们讲到在策略工厂类中如果每次都要返回新的策略对象我们还是需要在工厂类中编写if-else分支判断逻辑那这个问题该如何解决呢
欢迎留言和我分享你的想法。如果有收获,也欢迎你把这篇文章分享给你的朋友。