策略模式
该模式最常见的应用场景是,利用它来避免冗长的 if-else 或 switch 分支判断。不过,它的作用还不止如此。它也可以像模板模式那样,提供框架的扩展点等等。
1) 原理和实现
策略模式,英文全称是 Strategy Design Pattern。该模式是这样定义的:
Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it。
翻译成中文就是:定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的客户端(这里的客户端代指使用算法的代码)。
策略模式主要包含以下角色:
- 策略接口(Strategy):定义所有支持的算法的公共接口。客户端使用这个接口与具体策略进行交互。
- 具体策略(Concrete Strategy):实现策略接口的具体策略类。这些类封装了实际的算法逻辑。
- 上下文(Context):持有一个策略对象,用于与客户端进行交互。上下文可以定义一些接口,让客户端不直接与策略接口交互,从而实现策略的封装。
让我们以一个简单的例子来说明策略模式:假设我们要实现一个计算器,支持加法、减法和乘法运算 。我们可以使用策略模式将各种运算独立为不同的策略,并让客户端根据需要选择和使用不同的策略。
首先,定义一个策略接口Operation
:
java
public interface Operation {
double execute(double num1, double num2);
}
接下来,创建具体策略类来实现加法、减法和乘法运算:
java
public class Addition implements Operation {
@Override
public double execute(double num1, double num2) {
return num1 + num2;
}
}
public class Subtraction implements Operation {
@Override
public double execute(double num1, double num2) {
return num1 - num2;
}
}
public class Multiplication implements Operation {
@Override
public double execute(double num1, double num2) {
return num1 * num2;
}
}
然后,创建一个上下文类Calculator
,让客户端可以使用这个类来执行不同的运算:
java
public class Calculator {
// 客户端可以持有一个算法
private Operation operation;
public void setOperation(Operation operation) {
this.operation = operation;
}
public double calculate(double num1, double num2) {
return operation.execute(num1, num2);
}
}
现在,客户端可以使用Calculator
类来执行不同的运算,例如:
java
public class Client {
public static void main(String[] args) {
// 定义一个调用方
Calculator calculator = new Calculator();
// 计算加法
Operation operation = new Addition();
calculator.setOperation(operation);
double calculate = calculator.calculate(10, 12.9);
System.out.println("calculate = " + calculate);
// 计算减法
operation = new Subtraction();
calculator.setOperation(operation);
calculate = calculator.calculate(10, 12.9);
System.out.println("calculate = " + calculate);
}
}
在这个例子中,我们使用策略模式将加法、减法和乘法运算独立为不同的策略。客户端可以根据需要选择和使用不同的策略。Calculator
上下文类持有一个Operation
策略对象,并通过setOperation
方法允许客户端设置所需的策略。这种方式使得算法的选择和执行更加灵活,易于扩展和维护。
策略模式的优点包括:
- 提高代码的可维护性和可扩展性。当需要添加新的算法时,我们只需要实现一个新的具体策略类,而无需修改客户端代码。
- 符合开闭原则。策略模式允许我们在不修改现有代码的情况下引入新的策略。
- 避免使用多重条件判断。使用策略模式可以消除一些复杂的条件判断语句,使代码更加清晰和易于理解。
策略模式的缺点包括:
- 客户端需要了解所有的策略。为了选择合适的策略,客户端需要了解不同策略之间的区别。
- 增加了类的数量。策略模式会导致程序中具体策略类的数量增加,这可能会导致代码的复杂性增加。
在实际开发中,我们可以根据业务需求和系统架构灵活地运用策略模式。例如,在电商系统中,我们可以使用策略模式处理不同的促销策略;在游戏系统中,我们可以使用策略模式处理不同的角色行为等。
1、策略的定义
策略类的定义比较简单,包含一个策略接口和一组实现这个接口的策略类。因为所有的策略类都实现相同的接口,所以,客户端代码基于接口而非实现编程,可以灵活地替换不同的策略。示例代码如下所示:
java
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 创建策略的逻辑抽离出来,放到工厂类中。示例代码如下所示:
java
public class StrategyFactory {
private static final Map<String, Strategy> strategies = new HashMap<>();
static {
strategies.put("A", new ConcreteStrategyA());
strategies.put("B", new ConcreteStrategyB());
}
public static Strategy getStrategy(String type) {
if (type == null || type.isEmpty()) {
throw new IllegalArgumentException("type should not be empty.");
}
return strategies.get(type);
}
}
一般来讲,如果策略类是无状态的,不包含成员变量,只是纯粹的算法实现 ,这样的策略对象是可以被共享使用的,不需要在每次调用 getStrategy() 的时候,都创建一个新的策略对象。针对这种情况,我们可以使用上面这种工厂类的实现方式,事先创建好每个策略对象,缓存到工厂类中,用的时候直接返回。
相反,如果策略类是有状态的,根据业务场景的需要,我们希望每次从工厂方法中,获得的都是新创建的策略对象,而不是缓存好可共享的策略对象,那我们就需要按照如下方式来实现策略工厂类。
java
public class StrategyFactory {
public static Strategy getStrategy(String type) {
if (type == null || type.isEmpty()) {
throw new IllegalArgumentException("type should not be empty.");
}
if (type.equals("A")) {
return new ConcreteStrategyA();
} else if (type.equals("B")) {
return new ConcreteStrategyB();
}
return null;
}
}
3、策略的使用
刚刚讲了策略的定义和创建,现在,我们再来看一下,策略的使用。
我们知道,策略模式包含一组可选策略,客户端代码一般如何确定使用哪个策略呢?最常见的是运行时动态确定使用哪种策略,这也是策略模式最典型的应用场景。
这里的"运行时动态"指的是,我们事先并不知道会使用哪个策略,而是在程序运行期间,根据配置、用户输入、计算结果等这些不确定因素,动态决定使用哪种策略。接下来,我们通过一个例子来解释一下。
java
// 策略接口:EvictionStrategy
// 策略类:LruEvictionStrategy、FifoEvictionStrategy、LfuEvictionStrategy...
// 策略工厂:EvictionStrategyFactory
public class UserCache {
private Map<String, User> cacheData = new HashMap<>();
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("./config.properties"));
String type = props.getProperty("eviction_type");
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 中的使用方式,并不能发挥策略模式的优势。在这种应用场景下,策略模式实际上退化成了"面向对象的多态特性"或"基于接口而非实现编程原则",这其实就是开篇时列举的例子的场景。