在软件开发中,经常会遇到需要根据不同的条件来实现不同行为的场景。这种场景下,策略模式(Strategy Pattern)就是一种非常有用的设计模式。
那什么是策略模式呢?咱们先看定义
定义:策略是一种行为设计模式, 它能让你定义一系列算法, 并将每种算法分别放入独立的类中,以使算法的对象能够相互替换。
其UML结构为:
Context(上下文)
│
Strategy(抽象策略)
│
┌───┴──────────┐
ConcreteStrategyA ConcreteStrategyB(具体策略)
组成部分
- Strategy(策略接口):定义所有策略的公共方法。
- ConcreteStrategy(具体策略):实现
Strategy
,提供具体算法。 - Context(上下文):持有
Strategy
,并在运行时调用其方法。 - Client(客户端):选择合适的
Strategy
并传递给Context
。
应用场景
策略模式适用于以下场景:
✔ 支付方式(如支付宝、微信、信用卡支付)。
✔ 排序算法(如冒泡排序、快速排序、归并排序)。
✔ 日志记录(如本地日志、远程日志、数据库日志)。
✔ 数据压缩(如 ZIP、RAR、GZIP)。
✔ 促销策略(如满减、折扣、积分兑换)。
以上是策略模式的理论知识,相信大家看到这里,也是云里雾里吧。Talk enough,show me the code!
接下来,我将以实践形式,以折扣为场景手动带大家进一步加深印象。
首先,我们定义一个抽象接口类 MemberStrategy
public interface MemberStrategy {
// 一个计算价格的抽象方法
//price商品的价格 n商品的个数
double calcPrice(double price, int n);
}
然后,我们再创建其实现类(假设我们有普通会员,中级会员和高级会员,对应分别是不打折、打九折以及打八折)
// 普通会员——不打折
public class PrimaryMemberStrategy implements MemberStrategy {
@Override
public double calcPrice(double price, int n) {
return price * n;
}
}
// 中级会员 打百分之10的折扣
public class IntermediateMemberStrategy implements MemberStrategy{
@Override
public double calcPrice(double price, int n) {
double money = (price * n) - price * n * 0.1;
return money;
}
}
// 高级会员类 20%折扣
public class AdvanceMemberStrategy implements MemberStrategy{
@Override
public double calcPrice(double price, int n) {
double money = price * n - price * n * 0.2;
return money;
}
}
这三种不同的策略类实现了同一个抽象策略类,每种策略对应一种实现,分别应对一个业务处理方式。
最后,我们还需要定义一个上下文类/环境类,它是负责和具体的策略类交互。
/**
* 负责和具体的策略类交互
* 这样的话,具体的算法和直接的客户端调用分离了,使得算法可以独立于客户端独立的变化。
*/
// 上下文类/环境类
public class MemberContext {
// 用户折扣策略接口
private MemberStrategy memberStrategy; // 关键:持有策略接口
// 注入构造方法
public MemberContext(MemberStrategy memberStrategy) {
this.memberStrategy = memberStrategy;
}
// 计算价格
public double qoutePrice(double goodsPrice, int n){
// 此处可以添加统一逻辑(例如参数校验、日志等)
// 通过接口变量调用对应的具体策略
return memberStrategy.calcPrice(goodsPrice, n);
}
}
接下来,我们进行测试
public class Test{
public static void main(String[] args) {
// 具体行为策略
MemberStrategy primaryMemberStrategy = new PrimaryMemberStrategy(); // 接口回调(向上转型)
MemberStrategy intermediateMemberStrategy = new IntermediateMemberStrategy();
MemberStrategy advanceMemberStrategy = new AdvanceMemberStrategy();
// 用户选择不同策略
MemberContext primaryContext = new MemberContext(primaryMemberStrategy);
MemberContext intermediateContext = new MemberContext(intermediateMemberStrategy);
MemberContext advanceContext = new MemberContext(advanceMemberStrategy);
//计算一本300块钱的书
System.out.println("普通会员的价格:"+ primaryContext.qoutePrice(300,1));// 普通会员:300
System.out.println("中级会员的价格:"+ intermediateContext.qoutePrice(300,1));// 中级会员 270
System.out.println("高级会员的价格:"+ advanceContext.qoutePrice(300,1));// 高级会员240
}
以上就是一个整体的演示了!
但是看到这里,有人就会说,我都 MemberStrategy primaryMemberStrategy = new PrimaryMemberStrategy();,那我为什么不直接 primaryMemberStrategy.calcPrice(300, 1)。
为什么我还得再 new MemberContext,然后再把 primaryMemberStrategy 传进去调用,怎么感觉有点多此一举呢
我觉得这块有疑问是正常的!在这个简单的示例中,确实可以直接用 primaryMemberStrategy.calcPrice(300, 1)
,而不需要通过 MemberContext
类。但策略模式的核心价值在于更复杂的场景,下面我解释为什么需要 MemberContext
以及策略模式的真正意义。
1. 当前简单场景的局限性
咱们的例子只有一种策略(普通会员),且直接暴露了具体策略类。这种情况下,策略模式确实显得冗余。但实际业务中通常会有:
-
多种策略动态切换(如普通会员/黄金会员/钻石会员)
-
策略的复杂预处理/后处理(如日志记录、参数校验、结果格式化等)
-
策略的集中管理(避免策略逻辑散落在代码各处)
2. MemberContext 的核心作用
public class MemberContext {
private MemberStrategy memberStrategy; // 关键:持有策略接口
public MemberContext(MemberStrategy memberStrategy) {
this.memberStrategy = memberStrategy; // 依赖注入
}
public double qoutePrice(double goodsPrice, int n) {
// 此处可以添加统一逻辑(例如参数校验、日志等)
return memberStrategy.calcPrice(goodsPrice, n);
}
}
优势体现:
-
解耦客户端与策略
-
客户端不需要知道具体策略类(如
PrimaryMemberStrategy
),只需通过接口与MemberContext
交互。 -
符合迪米特法则(最少知识原则)。
-
-
动态切换策略
// 根据用户类型动态切换策略
MemberStrategy strategy = user.isVIP() ? new PrimaryMemberStrategy() : new IntermediateMemberStrategy();
MemberContext context = new MemberContext(strategy);
context.qoutePrice(300, 1);
3. 扩展统一逻辑
如果所有策略都需要添加折扣日志:
public double qoutePrice(double goodsPrice, int n) {
double result = memberStrategy.calcPrice(goodsPrice, n);
System.out.println("折扣策略:" + memberStrategy.getClass().getSimpleName() + ",最终价格:" + result);
return result;
}
符合开闭原则
新增策略时(如 PrimaryMemberStrategy),只需扩展新类,无需修改 MemberContext
。
3. 直接调用策略的问题
如果直接调用 primaryMemberStrategy.calcPrice(300, 1)
:
-
业务逻辑散落各处:如果需要在计算前后加逻辑(如验证价格不能为负),需修改所有调用点。
-
耦合度高:客户端代码依赖具体策略类,违反面向接口编程原则。
-
难以维护:当策略增多时,代码会充满大量的
if-else
或switch-case
。
策略模式的本质是通过 Context 隔离策略的创建和使用。
============================================================================================================
【注】在实际开发项目中,我们更多的是"将 Strategy 放在配置类中配置,然后在代码中读取",也就是通过 外部化配置 来动态管理策略的实现,而不是在代码中硬编码 new
具体策略类。这样做的好处是 灵活性高、易于维护,尤其适合需要频繁切换策略或策略需要动态更新的场景。
1. 直接 new
策略的问题
原代码中直接在 main
方法里实例化策略:
MemberStrategy primaryMemberStrategy = new PrimaryMemberStrategy(); // 硬编码
缺点:
-
策略类名写死在代码中,修改策略需要重新编译。
-
无法在运行时动态切换策略(例如根据配置文件切换折扣策略)。
接下来,我将展示如何通过外部化配置方式实现~~
1. 定义策略配置属性类
通过 @ConfigurationProperties
绑定配置文件中的策略配置:
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "member.strategy") // 绑定配置前缀
public class MemberStrategyProperties {
private String active; // 对应 member.strategy.active
private Map<String, String> classes; // 可选:支持多策略映射
// Getters and Setters (必须)
public String getActive() {
return active;
}
public void setActive(String active) {
this.active = active;
}
public Map<String, String> getClasses() {
return classes;
}
public void setClasses(Map<String, String> classes) {
this.classes = classes;
}
}
2. 在 application.yml
中配置策略
# 测试专用配置
member:
strategy:
active: "primary"
classes:
primary: "com.example.test.策略模式.配置化.PrimaryMemberStrategy"
vip: "com.example.test.策略模式.配置化.IntermediateMemberStrategy"
platinum: "com.example.test.策略模式.配置化.AdvanceMemberStrategy"
3. 创建策略配置类
结合 @ConfigurationProperties
和策略工厂模式:
@Configuration
@Slf4j
public class MemberStrategyConfig {
private final MemberStrategyProperties properties;
// 自动注入配置属性
@Autowired
public MemberStrategyConfig(MemberStrategyProperties properties) {
this.properties = properties;
}
@Bean
public MemberStrategy memberStrategy() throws Exception {
// 根据配置的Key获取具体策略类名
String strategyClassName = properties.getClasses().get(properties.getActive());
log.info("正在加载策略类: {}", strategyClassName); // 添加日志
Class<?> clazz = Class.forName(strategyClassName);
log.info("策略类加载成功: {}", clazz.getName()); // 添加日志
if (!MemberStrategy.class.isAssignableFrom(clazz)) {
throw new IllegalStateException(strategyClassName + " 未实现 MemberStrategy 接口");
}
return (MemberStrategy) clazz.getDeclaredConstructor().newInstance();
}
}
4. 使用策略(通过自动注入)
@Service
public class OrderService {
private final MemberContext memberContext;
@Autowired
public OrderService(MemberStrategy memberStrategy) {
this.memberContext = new MemberContext(memberStrategy);
}
public double calculatePrice(double price, int count) {
return memberContext.qoutePrice(price, count);
}
}
补充:添加配置元数据(可选)
在 resources/META-INF
下创建 additional-spring-configuration-metadata.json
,提供配置提示:
{
"properties": [
{
"name": "member.strategy.active",
"type": "java.lang.String",
"description": "当前激活的会员策略Key",
"sourceType": "com.example.test.策略模式.配置化.config.MemberStrategyProperties"
},
{
"name": "member.strategy.classes",
"type": "java.util.Map<java.lang.String, java.lang.String>",
"description": "策略类全限定名映射",
"sourceType": "com.example.test.策略模式.配置化.config.MemberStrategyProperties"
}
]
}
依赖要求
确保 pom.xml
包含:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
5. 准备测试类
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@ActiveProfiles("test") // 激活test配置profile
class OrderServiceIntegrationTest {
@Autowired
private OrderService orderService;
@Test
void testPrimaryStrategy() {
// 根据test配置,应使用PrimaryMemberStrategy(原价)
double price = orderService.calculatePrice(100.0, 2);
System.out.println("primary会员价格:" + price);
// assertEquals(200.0, price);
}
}
运行
@ActiveProfiles("test")
是 Spring 测试框架中的一个注解,用于在测试时显式激活指定的 Spring 配置环境(Profile)。它的核心作用是告诉 Spring 在运行测试时,应该加载哪些特定的配置文件或配置段。
核心作用
-
隔离测试环境:让测试使用独立的配置(如测试数据库、Mock服务等),避免污染生产配置。
-
灵活切换配置:针对不同测试场景加载不同的配置(如
dev
/test
/prod
)。
工作原理
-
启动测试:Spring 会优先加载
application-test.yml
或application.yml
中标记为test
的配置段。
spring:
profiles: test # 标记为test环境
-
Bean覆盖:测试Profile中的配置会覆盖主配置中的同名属性。
-
依赖注入:所有
@Autowired
的Bean都会基于激活的Profile创建。
核心规则
Spring Boot 通过 文件名约定 自动关联 Profile,无需显式标记。具体规则:
-
文件命名格式为
application-{profile}.yml
(或.properties
) -
当使用
@ActiveProfiles("test")
时,会自动加载:-
application-test.yml
(高优先级) -
application.yml
中的通用配置(低优先级)
-
相信你读完这里,对策略模式有了更深的印象吧!