策略模式(Strategy Design Pattern), 定义的一族算法类,将每个算法分别封装起来,让它们可以互相替 换。策略模式可以使算法的变化独立于使用它们的客户端(这里的客户端代指使用算法的代码)。
策略模式主要包含以下角色:
简单案例说明:
假设我们要实现一个计算器,支持加法、 减法和乘法运算。我们可以使用策略模式将各种运算独立为不同的策略,并让客户端根据需要选择和使用不同的策略。
首先,我们定义一个策略接口 Operation :
package cn.itcast.designPatterns.strategy.demo1;
/**
* 类描述:抽象操作接口
*
* @Author crysw
* @Version 1.0
* @Date 2024/3/3 19:48
*/
public interface Operation {
double execute(double num1, double num2);
}
然后,创建具体策略类来实现加法、减法和乘法运算:
package cn.itcast.designPatterns.strategy.demo1;
/**
* 类描述:加操作
*
* @Author crysw
* @Version 1.0
* @Date 2024/3/3 19:49
*/
public class Addition implements Operation {
@Override
public double execute(double num1, double num2) {
return num1 + num2;
}
}
/**
* 类描述:减操作
*
* @Author crysw
* @Version 1.0
* @Date 2024/3/3 19:50
*/
public class Subtraction implements Operation {
@Override
public double execute(double num1, double num2) {
return num1 - num2;
}
}
/**
* 类描述:乘法操作
*
* @Author crysw
* @Version 1.0
* @Date 2024/3/3 19:51
*/
public class Multiplication implements Operation {
@Override
public double execute(double num1, double num2) {
return num1 * num2;
}
}
创建一个上下文类 Calculator ,让客户端可以使用这个类来执行不同的运算:
package cn.itcast.designPatterns.strategy.demo1;
/**
* 类描述:暴露给客户端使用的上下文类
*
* @Author crysw
* @Version 1.0
* @Date 2024/3/3 19:52
*/
public class Calculator {
private Operation operation;
public Calculator() {
}
public Calculator(Operation operation) {
this.operation = operation;
}
public void setOperation(Operation operation) {
this.operation = operation;
}
public double executeOperatiion(double num1, double num2) {
return operation.execute(num1, num2);
}
}
客户端测试案例:
/**
* 类描述:策略模式测试案例
*
* @Author crysw
* @Version 1.0
* @Date 2024/3/3 19:54
*/
@Slf4j
public class StrategyPatternTest {
@Test
public void testCalculator() {
double num1 = 6, num2 = 4;
Calculator calculator = new Calculator();
calculator.setOperation(new Addition());
double result = calculator.executeOperatiion(num1, num2);
log.info(">>>Addition:{}", result);
calculator.setOperation(new Subtraction());
result = calculator.executeOperatiion(num1, num2);
log.info(">>>Subtraction:{}", result);
calculator.setOperation(new Multiplication());
result = calculator.executeOperatiion(num1, num2);
log.info(">>>Multiplication:{}", result);
}
}
在这个例子中,使用策略模式将加法、减法和乘法运算独立为不同的策略。客户端可以根据需要选择和使用不同的策略。 Calculator 上下文类持有一个Operation 策略对象,并通过 setOperation 方法允许客户端设置所需的策略。
策略模式的优点:
策略模式的缺点:
策略类的定义比较简单,包含一个策略接口和一组实现这个接口的具体策略类。因为所有的策略类都实现相同的接口,所以客户端代码基于接口而非实现编程,可以灵活地替换不同的策略。示例代码如下所示:
// 策略接口
public interface Strategy {
void algorithmInterface();
}
// 具体策略A
public class ConcreteStrategyA implements Strategy {
@Override
public void algorithmInterface() {
//具体的算法...
}
}
// 具体策略B
public class ConcreteStrategyB implements Strategy {
@Override
public void algorithmInterface() {
//具体的算法...
}
}
因为策略模式会包含一组策略,在使用时会通过类型(type) 断创建哪个策略来使用。为了封装创建逻辑,我们需要对客户端代码屏蔽创建细节。
如果策略类是无状态的,不包含成员变量,只是纯粹的算法实现,这样的策略对象是可以被共享使用的,不需要在每次调用 getStrategy() 的时候,都创建一 个新的策略对象。可以把根据 type 创建策略的逻辑抽离出来,放到工 厂类中。示例代码如下所示:
// 创建策略的工厂
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);
}
}
如果策略类是有状态的,根据业务场景的需要,我们希望每次从工厂方法中, 获得的都是新创建的策略对象,而不是缓存好可共享的策略对象,那我们就需要按照 如下方式来实现策略工厂类。
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;
}
}
策略模式通常包含一组可选策略,客户端代码如何确定使用哪个策略呢? 最常见的是运行时动态确定使用哪种策略,这也是策略模式最典型的应用场景。
这里的“运行时动态”指的是,我们事先并不知道会使用哪个策略,而是在程序运行期间,根据配置、用户输入、计算结果等这些不确定因素,动态决定使用哪种策略。
// 策略接口: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 中的使用方式,并不能发挥策略模式的优势。在这种应用场景下,策略模式实际上退化成了“面向对象的多态特性”或“基于接口而非实现编程原则”,这其实就是开篇时列举的例子场景。
下边这段代码是一个解析不同来源报文的方法,用了1000 多行。针对这样的代码,我们如何优化呢?使用工厂模式和策略设计模式优化代码。
以解析报文案例为基础,展示如何使用策略模式优化具有 if 条件分支的代码。 首先这是一个包含大量 if 分支的报文解析系统:
package cn.itcast.designPatterns.strategy.demo2;
import lombok.extern.slf4j.Slf4j;
/**
* 类描述:报文解析
*
* @Author crysw
* @Version 1.0
* @Date 2024/3/4 20:43
*/
@Slf4j
public class MessageParser {
public void parseMessage(Message message) {
String messageType = message.getType();
if ("XML".equalsIgnoreCase(messageType)) {
// 解析XML报文
log.info("解析XML报文:{}", message.getContent());
} else if ("JSON".equalsIgnoreCase(messageType)) {
// 解析 JSON 报文
log.info("解析 JSON 报文: {}", message.getContent());
} else if ("CSV".equalsIgnoreCase(messageType)) {
// 解析CSV报文
log.info("解析CSV报文:{}", message.getContent());
} else {
throw new IllegalArgumentException("未知的报文类型:" + messageType);
}
}
}
使用策略模式优化上述代码。 首先定义一个报文解析策略接口 MessageParserStrategy :
package cn.itcast.designPatterns.strategy.demo2;
/**
* 接口描述:报文解析策略接口
*
* @Author crysw
* @Version 1.0
* @Date 2024/3/4 20:51
*/
public interface MessageParserStrategy {
/**
* 解析报文的方法
*
* @param message
*/
void parse(Message message);
}
实现 XML、JSON 和 CSV 报文解析策略:
package cn.itcast.designPatterns.strategy.demo2;
import lombok.extern.slf4j.Slf4j;
/**
* 类描述:XML报文解析策略的具体实现
*
* @Author crysw
* @Version 1.0
* @Date 2024/3/4 20:52
*/
@Slf4j
public class XmlMessageParserStrategy implements MessageParserStrategy {
@Override
public void parse(Message message) {
log.info("解析XML报文:{}", message.getContent());
}
}
/**
* 类描述:JSON报文解析策略的具体实现
*
* @Author crysw
* @Version 1.0
* @Date 2024/3/4 20:52
*/
@Slf4j
public class JsonMessageParserStrategy implements MessageParserStrategy {
@Override
public void parse(Message message) {
log.info("解析 JSON 报文: {}", message.getContent());
}
}
/**
* 类描述:CSV报文解析策略的具体实现
*
* @Author crysw
* @Version 1.0
* @Date 2024/3/4 20:54
*/
@Slf4j
public class CsvMessageParserStrategy implements MessageParserStrategy {
@Override
public void parse(Message message) {
log.info("解析CSV报文:{}", message.getContent());
}
}
创建一个 MessageParserService类,该类将根据传入的策略解析报文:
package cn.itcast.designPatterns.strategy.demo2;
/**
* 类描述:暴露的报文解析服务
*
* @Author crysw
* @Version 1.0
* @Date 2024/3/4 20:54
*/
public class MessageParserService {
private MessageParserStrategy strategy;
/**
* 设置报文解析策略
*
* @param strategy
*/
public void setStrategy(MessageParserStrategy strategy) {
this.strategy = strategy;
}
/**
* 根据选择的策略解析报文
*
* @param message
*/
public void parseMessage(Message message) {
this.strategy.parse(message);
}
}
测试案例:
@Slf4j
public class StrategyPatternTest {
@Test
public void testMessageParserStrategy() {
MessageParserService parserContext = new MessageParserService();
// 使用 XML 报文解析策略
parserContext.setStrategy(new XmlMessageParserStrategy());
parserContext.parseMessage(Message.builder().type("XML").content("这是一个 XML 报文 ").build());
// 使用 JSON 报文解析策略
parserContext.setStrategy(new JsonMessageParserStrategy());
parserContext.parseMessage(Message.builder().type("JSON").content("{\"message\": \"这是一个 JSON 报文\"}").build());
// 使用 CSV 报文解析策略
parserContext.setStrategy(new CsvMessageParserStrategy());
parserContext.parseMessage(Message.builder().type("CSV").content("这是一个,CSV,报文").build());
}
}
我们可以将策略模式与工厂模式结合,以便根据不同的消息类型自动匹配不同的解析策略。下面是如何实现这个优化的:
创建一个 MessageParserStrategyFactory 类,用于根据报文类型创建相应的解析策略:
package cn.itcast.designPatterns.strategy.demo2;
import java.util.HashMap;
import java.util.Map;
/**
* 类描述:报文解析策略工厂
*
* @Author crysw
* @Version 1.0
* @Date 2024/3/4 21:02
*/
public class MessageParserStrategyFactory {
private static final Map<String, MessageParserStrategy> STRATEGIES = new HashMap<>();
static {
STRATEGIES.put("XML", new XmlMessageParserStrategy());
STRATEGIES.put("JSON", new JsonMessageParserStrategy());
STRATEGIES.put("CSV", new CsvMessageParserStrategy());
}
/**
* 根据报文类型获取报文解析策略
*
* @param messageType
* @return
*/
public static MessageParserStrategy getStrategy(String messageType) {
MessageParserStrategy strategy = STRATEGIES.get(messageType.toUpperCase());
if (strategy == null) {
throw new IllegalArgumentException("未知的报文类型:" + messageType);
}
return strategy;
}
}
创建新的服务MessageParserServiceV2,提供解析报文的入口:
package cn.itcast.designPatterns.strategy.demo2;
/**
* 类描述:暴露的报文解析服务2
*
* @Author crysw
* @Version 1.0
* @Date 2024/3/4 21:08
*/
public class MessageParserServiceV2 {
/**
* 根据选择的策略解析报文
*
* @param message
*/
public void parseMessage(Message message) {
MessageParserStrategyFactory.getStrategy(message.getType()).parse(message);
}
}
测试案例2:
@Slf4j
public class StrategyPatternTest {
@Test
public void testMessageParserStrategyV2() {
MessageParserServiceV2 parserContext = new MessageParserServiceV2();
// 使用 XML 报文解析策略
parserContext.parseMessage(Message.builder().type("XML").content("这是一个 XML 报文 ").build());
// 使用 JSON 报文解析策略
parserContext.parseMessage(Message.builder().type("JSON").content("{\"message\": \"这是一个 JSON 报文\"}").build());
// 使用 CSV 报文解析策略
parserContext.parseMessage(Message.builder().type("CSV").content("这是一个,CSV,报文").build());
}
}
策略模式通过将算法和客户端分离, 使得系统更加灵活和可扩展。在实际开发中,我们可以参考这些例子,根据业务需求和系统架构灵活地运用策略模式。
以 java.util.Comparator 接口为例,展示策略模式在JDK中的应用。 假设有一个 Student 学生类,我们需要对一个 Student 对象的列表进行排序(可能需要按照学生的姓名、年龄或成绩进行排序)。我们可以使用策略模式,通过实现 Comparator 接口,为不同的排序需求提供不同的比较策略。
首先定义Student类
public class Student {
private String name;
private int age;
private double score;
// 构造方法、getter和setter方法省略
}
实现 Comparator 接口,定义不同的比较策略:
public class NameComparator implements Comparator<Student> {
@Override
public int compare(Student s1, Student s2) {
return s1.getName().compareTo(s2.getName());
}
}
// 根据学生的年龄进行排序
public class AgeComparator implements Comparator<Student> {
@Override
public int compare(Student s1, Student s2) {
return Integer.compare(s1.getAge(), s2.getAge());
}
}
// 根据学生的成绩进行排序
public class ScoreComparator implements Comparator<Student> {
@Override
public int compare(Student s1, Student s2) {
return Double.compare(s1.getScore(), s2.getScore());
}
}
在客户端代码中,根据需要选择和使用不同的比较策略:
public class Client {
public static void main(String[] args) {
// 创建一个Student对象的列表
List<Student> students = new ArrayList<>();
students.add(new Student("Alice", 20, 90.0));
students.add(new Student("Bob", 18, 85.0));
students.add(new Student("Charlie", 22, 88.0));
// 使用姓名比较策略进行排序
Collections.sort(students, new NameComparator());
System.out.println("按姓名排序: " + students);
// 使用年龄比较策略进行排序
Collections.sort(students, new AgeComparator());
System.out.println("按年龄排序: " + students);
// 使用成绩比较策略进行排序
Collections.sort(students, new ScoreComparator());
System.out.println("按成绩排序: " + students);
}
}
这个例子中使用策略模式将不同的排序策略独立为不同的类。客户端可以根据需要选择和使用不同的排序策略,而无需修改代码。
使用策略模式的支付系统的简化示例。
首先定义一个支付策略接口 PaymentStrategy
package cn.itcast.designPatterns.strategy.demo3;
/**
* 接口描述:支付策略接口
*
* @Author crysw
* @Version 1.0
* @Date 2024/3/10 17:47
*/
public interface PaymentStrategy {
/**
* 支付操作
*
* @param amount
*/
void pay(double amount);
}
具体的策略实现
package cn.itcast.designPatterns.strategy.demo3;
import lombok.extern.slf4j.Slf4j;
/**
* 类描述:支付宝支付策略实现
*
* @Author crysw
* @Version 1.0
* @Date 2024/3/10 17:51
*/
@Slf4j
public class AlipayPaymentStrategy implements PaymentStrategy {
private String email;
private String password;
public AlipayPaymentStrategy(String email, String password) {
this.email = email;
this.password = password;
}
@Override
public void pay(double amount) {
log.info(">>> 支付宝支付:{}元.", amount);
}
}
/**
* 类描述:信用卡支付策略实现
*
* @Author crysw
* @Version 1.0
* @Date 2024/3/10 17:48
*/
@Slf4j
public class CreditCardPaymentStrategy implements PaymentStrategy {
private String name;
private String cardNumber;
private String cvv;
private String expirationDate;
public CreditCardPaymentStrategy(String name, String cardNumber, String cvv, String expirationDate) {
this.name = name;
this.cardNumber = cardNumber;
this.cvv = cvv;
this.expirationDate = expirationDate;
}
@Override
public void pay(double amount) {
log.info(">>> 信用卡支付:{}元.", amount);
}
}
测试案例
@Test
public void testPaymentStrategy() {
// 创建信用卡支付策略实例
PaymentStrategy creditCardStrategy = new CreditCardPaymentStrategy("张三", "15616221523", "1230", "12/23");
// 创建支付宝支付策略实例
PaymentStrategy alipayStrategy = new AlipayPaymentStrategy("[email protected]", "mypassword");
// 根据用户选择的支付方式进行支付
double amount = 100.33;
creditCardStrategy.pay(amount);
alipayStrategy.pay(amount);
}
在这个示例中定义了一个支付策略接口 PaymentStrategy ,并为不同的支付方式提供了具体实现。客户端代码可以根据用户选择的支付方式来调用相应的支付策略。当需要添加新的支付方式时,只需提供新的支付策略实现,而无需修改客户端代码,从而提高了系统的可扩展性。
使用策略模式的促销系统的简化示例
首先定义一个促销策略接口 PromotionStrategy :
package cn.itcast.designPatterns.strategy.demo4;
/**
* 类描述:优惠策略接口
*
* @Author crysw
* @Version 1.0
* @Date 2024/3/10 17:57
*/
public interface PromotionStrategy {
/**
* 计算优惠后的价格
*
* @param originalPrice
* @return
*/
double calculateDiscount(double originalPrice);
}
满减优惠方式的策略实现
package cn.itcast.designPatterns.strategy.demo4;
import lombok.extern.slf4j.Slf4j;
/**
* 类描述:满减优惠方式的策略实现
*
* @Author crysw
* @Version 1.0
* @Date 2024/3/10 17:58
*/
@Slf4j
public class FullReductionStrategy implements PromotionStrategy {
/**
* 初始价格
*/
private double threshold;
/**
* 满减金额
*/
private double reduction;
public FullReductionStrategy(double threshold, double reduction) {
this.threshold = threshold;
this.reduction = reduction;
}
@Override
public double calculateDiscount(double originalPrice) {
return originalPrice >= this.threshold ? originalPrice - reduction : originalPrice;
}
}
打折优惠方式的策略实现
package cn.itcast.designPatterns.strategy.demo4;
/**
* 类描述:打折优惠方式的策略实现
*
* @Author crysw
* @Version 1.0
* @Date 2024/3/10 18:01
*/
public class DiscountStrategy implements PromotionStrategy {
private double discount;
public DiscountStrategy(double discount) {
this.discount = discount;
}
@Override
public double calculateDiscount(double originalPrice) {
return originalPrice * this.discount;
}
}
赠品策略实现
package cn.itcast.designPatterns.strategy.demo4;
import lombok.extern.slf4j.Slf4j;
/**
* 类描述:新的促销方式策略(购买指定商品后可以获得免费赠品)
*
* @Author crysw
* @Version 1.0
* @Date 2024/3/10 18:08
*/
@Slf4j
public class GiftStrategy implements PromotionStrategy {
private double targetPrice;
private String giftName;
public GiftStrategy(double targetPrice, String giftName) {
this.targetPrice = targetPrice;
this.giftName = giftName;
}
@Override
public double calculateDiscount(double originalPrice) {
if (originalPrice >= targetPrice) {
log.info(">>> 获得赠品:{}", giftName);
}
return originalPrice;
}
}
无优惠的策略实现
package cn.itcast.designPatterns.strategy.demo4;
/**
* 类描述:无优惠的策略实现
*
* @Author crysw
* @Version 1.0
* @Date 2024/3/10 18:02
*/
public class NoDiscountStrategy implements PromotionStrategy {
@Override
public double calculateDiscount(double originalPrice) {
return originalPrice;
}
}
测试案例
@Test
public void testPromotionStrategy() {
// 创建满减策略实例
PromotionStrategy fullReductionStrategy = new FullReductionStrategy(300, 100);
// 创建打折策略实例
PromotionStrategy discountStrategy = new DiscountStrategy(0.8);
// 创建无优惠策略实例
PromotionStrategy noDiscountStrategy = new NoDiscountStrategy();
// 根据用户选择的促销方式计算优惠后的价格
double originalPrice = 350.0;
log.info(">>> 用户选择满减优惠, 优惠后的价格是:{}元", fullReductionStrategy.calculateDiscount(originalPrice));
log.info(">>> 用户选择打折优惠, 优惠后的价格是:{}元", discountStrategy.calculateDiscount(originalPrice));
log.info(">>> 用户无优惠, 价格是:{}元", noDiscountStrategy.calculateDiscount(originalPrice));
log.info("===============================");
// 创建赠品策略实例
PromotionStrategy giftStrategy = new GiftStrategy(500, "公仔熊");
originalPrice = 550.0;
log.info(">>> 用户选择赠品优惠, 优惠后的价格是:{}元", giftStrategy.calculateDiscount(originalPrice));
}
提到 if-else 分支判断,有人就觉得是烂代码。如果 if-else 分支判断不复杂、代码不多,并没有任何问题,毕竟 if-else 分支判断几乎是所有编程语言都会提供的语法。遵循KISS原则,简单就是最好的设计。非得用策略模式,搞出 n 多个类反倒是一种过度设计。
提到策略模式,有人就觉得它的作用是避免 if-else 分支判断逻辑。这种认识是很片面的。策略模式的主要作用是解耦策略的定义、创建和使用,控制代码的复杂度,让每个部分都不至于过于复杂、代码量过多。除此之外,对于复杂代码来说,策略模式还能让其满足开闭原则,添加新策略的时候,最小化、集中化代码改动,减少引入 bug 的风险。
策略模式定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的客户端(这里的客户端代指使用算法的代码)。
策略模式用来解耦策略的定义、创建、使用。 一个完整的策略模式就是由下面三个部分组成:
除此之外,我们还可以通过策略模式来移除 if-else 分支判断。 这得益于策略工厂类,是借助“查表法”,根据 type 查表替代根据 type 分支判断。