在Java后端开发中,我们经常会遇到多分支判断的场景——比如根据不同的支付方式处理订单、根据不同的用户等级计算积分、根据不同的业务类型生成报表等。最初的实现往往是堆砌if-else语句,这种代码不仅可读性差、难以维护,还违反了开闭原则,新增分支时需要修改原有代码,极易引发bug。而策略模式,正是解决这类“多分支判断臃肿代码”的最优方案之一。
本文将从策略模式的核心定义出发,深入剖析其底层设计逻辑,结合3个不同复杂度的真实业务场景(支付方式适配、会员积分计算、动态规则校验),提供可直接编译运行的完整代码实现,同时解答开发中关于策略模式选型、与其他模式区别、性能优化等关键问题,帮你真正掌握策略模式的落地技巧。
一、策略模式核心认知:是什么、为什么用、底层逻辑
1.1 策略模式的定义
策略模式(Strategy Pattern)是一种行为型设计模式,其核心思想是:定义一组算法(策略),将每个算法封装起来,使它们可以相互替换,并且算法的变化不会影响使用算法的客户端。
换句话说,策略模式就是把复杂业务场景中的不同处理逻辑(策略)抽离出来,独立封装成一个个策略类,客户端通过统一的入口选择不同的策略执行,从而避免多分支判断。
1.2 为什么需要策略模式?—— 解决if-else痛点
我们先看一个典型的“反例”:未使用策略模式的支付方式处理代码。
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
/**
* 未使用策略模式的支付处理类
* @author ken
*/
@Slf4j
public class PaymentService {
/**
* 处理支付
* @param orderId 订单ID
* @param amount 支付金额
* @param payType 支付方式:ALIPAY(支付宝)、WECHAT(微信)、UNIONPAY(银联)
* @return 支付结果
*/
public String processPayment(String orderId, BigDecimal amount, String payType) {
// 校验参数
if (StringUtils.hasText(orderId, "订单ID不能为空")) {
throw new IllegalArgumentException("订单ID不能为空");
}
if (ObjectUtils.isEmpty(amount) || amount.compareTo(BigDecimal.ZERO) <= 0) {
log.error("支付金额非法,订单ID:{},金额:{}", orderId, amount);
throw new IllegalArgumentException("支付金额必须大于0");
}
if (StringUtils.hasText(payType, "支付方式不能为空")) {
throw new IllegalArgumentException("支付方式不能为空");
}
// 多分支判断处理不同支付方式
if ("ALIPAY".equals(payType)) {
log.info("执行支付宝支付,订单ID:{},金额:{}", orderId, amount);
// 支付宝支付逻辑...
return "支付宝支付成功,订单ID:" + orderId;
} else if ("WECHAT".equals(payType)) {
log.info("执行微信支付,订单ID:{},金额:{}", orderId, amount);
// 微信支付逻辑...
return "微信支付成功,订单ID:" + orderId;
} else if ("UNIONPAY".equals(payType)) {
log.info("执行银联支付,订单ID:{},金额:{}", orderId, amount);
// 银联支付逻辑...
return "银联支付成功,订单ID:" + orderId;
} else {
log.error("不支持的支付方式,订单ID:{},支付方式:{}", orderId, payType);
throw new UnsupportedOperationException("不支持的支付方式:" + payType);
}
}
}
这段代码存在明显问题:
- 可读性差:随着支付方式增加(比如新增ApplePay、GooglePay),if-else分支会持续膨胀,代码冗长混乱;
- 可维护性差:修改某一种支付逻辑时,需要直接修改
processPayment方法,违反“开闭原则”(对扩展开放、对修改关闭); - 可测试性差:一个方法包含多种逻辑,单元测试需要覆盖所有分支,测试用例复杂;
- 耦合度高:支付逻辑与分支判断逻辑强耦合,不利于代码复用。
而使用策略模式重构后,上述问题将得到彻底解决。
1.3 策略模式的底层逻辑与核心角色
策略模式的底层逻辑是基于“封装变化”和“依赖倒置”原则,通过将可变的算法(策略)封装为独立类,使客户端依赖于抽象策略而非具体实现,从而实现算法的灵活替换和扩展。
策略模式包含3个核心角色:
- 抽象策略(Strategy):定义策略的统一接口,声明所有具体策略都需要实现的核心方法(如支付方法);
- 具体策略(ConcreteStrategy):实现抽象策略接口,封装具体的算法逻辑(如支付宝支付、微信支付的具体实现);
- 策略上下文(Context):作为客户端与策略的中间层,负责持有和管理策略对象,提供统一的入口供客户端调用策略,避免客户端直接与具体策略耦合。
其核心交互流程如下:
通过这一结构,客户端只需通过上下文指定策略类型,无需关注具体策略的实现细节;新增策略时,只需实现抽象策略接口并注册到上下文,无需修改原有代码,完美符合开闭原则。
二、策略模式的基础实现:支付方式适配场景
下面以“多支付方式适配”为例,展示策略模式的基础实现流程,代码基于JDK 17、Spring Boot 3.2.0、Lombok 1.18.30实现,确保可直接编译运行。
2.1 步骤1:定义抽象策略接口(支付策略)
抽象策略接口定义所有支付方式的统一规范,声明支付核心方法。
package com.jam.demo.strategy.payment;
import java.math.BigDecimal;
/**
* 支付策略抽象接口
* @author ken
*/
public interface PaymentStrategy {
/**
* 获取支付方式编码(如ALIPAY、WECHAT)
* @return 支付方式编码
*/
String getPayType();
/**
* 执行支付
* @param orderId 订单ID
* @param amount 支付金额
* @return 支付结果
*/
String pay(String orderId, BigDecimal amount);
}
2.2 步骤2:实现具体策略类(不同支付方式)
分别实现支付宝、微信、银联三种支付方式的具体逻辑,每个类专注于自身的支付算法。
2.2.1 支付宝支付策略
package com.jam.demo.strategy.payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
/**
* 支付宝支付具体策略
* @author ken
*/
@Slf4j
@Component // 交给Spring管理,便于后续自动注入
public class AlipayStrategy implements PaymentStrategy {
@Override
public String getPayType() {
return "ALIPAY";
}
@Override
public String pay(String orderId, BigDecimal amount) {
// 模拟支付宝支付核心逻辑:调用支付宝SDK、签名验证、发起支付等
log.info("【支付宝支付】开始处理订单支付,订单ID:{},支付金额:{}", orderId, amount);
// 此处可添加真实的支付宝支付SDK调用逻辑
log.info("【支付宝支付】订单支付成功,订单ID:{}", orderId);
return "支付宝支付成功,订单ID:" + orderId + ",支付金额:" + amount;
}
}
2.2.2 微信支付策略
package com.jam.demo.strategy.payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
/**
* 微信支付具体策略
* @author ken
*/
@Slf4j
@Component
public class WechatPayStrategy implements PaymentStrategy {
@Override
public String getPayType() {
return "WECHAT";
}
@Override
public String pay(String orderId, BigDecimal amount) {
// 模拟微信支付核心逻辑
log.info("【微信支付】开始处理订单支付,订单ID:{},支付金额:{}", orderId, amount);
// 此处可添加真实的微信支付SDK调用逻辑
log.info("【微信支付】订单支付成功,订单ID:{}", orderId);
return "微信支付成功,订单ID:" + orderId + ",支付金额:" + amount;
}
}
2.2.3 银联支付策略
package com.jam.demo.strategy.payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
/**
* 银联支付具体策略
* @author ken
*/
@Slf4j
@Component
public class UnionPayStrategy implements PaymentStrategy {
@Override
public String getPayType() {
return "UNIONPAY";
}
@Override
public String pay(String orderId, BigDecimal amount) {
// 模拟银联支付核心逻辑
log.info("【银联支付】开始处理订单支付,订单ID:{},支付金额:{}", orderId, amount);
// 此处可添加真实的银联支付SDK调用逻辑
log.info("【银联支付】订单支付成功,订单ID:{}", orderId);
return "银联支付成功,订单ID:" + orderId + ",支付金额:" + amount;
}
}
2.3 步骤3:实现策略上下文(支付上下文)
策略上下文负责管理所有具体策略,提供统一的支付入口,客户端通过上下文调用对应策略的支付方法。这里利用Spring的自动注入特性,将所有PaymentStrategy实现类注入到Map中,key为支付方式编码,value为具体策略对象,实现策略的自动注册和快速获取。
package com.jam.demo.strategy.payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 支付策略上下文
* 负责管理支付策略,提供统一支付入口
* @author ken
*/
@Slf4j
@Component
public class PaymentContext {
/**
* 存储所有支付策略,key:支付方式编码(如ALIPAY),value:对应支付策略对象
* 使用ConcurrentHashMap保证线程安全
*/
private final Map<String, PaymentStrategy> paymentStrategyMap;
/**
* 构造方法自动注入所有PaymentStrategy实现类,Spring会将所有实现类放入Map中
* key为getPayType()返回的支付方式编码,value为策略对象
* @param strategies 所有支付策略实现类
*/
@Autowired
public PaymentContext(Map<String, PaymentStrategy> strategies) {
this.paymentStrategyMap = new ConcurrentHashMap<>(strategies);
log.info("初始化支付策略Map,加载策略数量:{},策略详情:{}", strategies.size(), strategies.keySet());
}
/**
* 统一支付入口
* @param orderId 订单ID
* @param amount 支付金额
* @param payType 支付方式编码
* @return 支付结果
*/
public String processPayment(String orderId, BigDecimal amount, String payType) {
// 参数校验
if (!StringUtils.hasText(orderId)) {
throw new IllegalArgumentException("订单ID不能为空");
}
if (ObjectUtils.isEmpty(amount) || amount.compareTo(BigDecimal.ZERO) <= 0) {
log.error("支付金额非法,订单ID:{},金额:{}", orderId, amount);
throw new IllegalArgumentException("支付金额必须大于0");
}
if (!StringUtils.hasText(payType)) {
throw new IllegalArgumentException("支付方式不能为空");
}
// 根据支付方式获取对应的策略
PaymentStrategy strategy = paymentStrategyMap.get(payType);
if (ObjectUtils.isEmpty(strategy)) {
log.error("不支持的支付方式,订单ID:{},支付方式:{}", orderId, payType);
throw new UnsupportedOperationException("不支持的支付方式:" + payType);
}
// 调用策略的支付方法
return strategy.pay(orderId, amount);
}
}
2.4 步骤4:客户端调用(Controller层)
客户端(这里是Controller)通过调用策略上下文的统一入口,传入支付方式参数,即可完成对应支付方式的调用,无需关注具体策略的实现。
package com.jam.demo.controller;
import com.jam.demo.strategy.payment.PaymentContext;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
/**
* 支付控制器(客户端调用入口)
* @author ken
*/
@Slf4j
@RestController
@RequestMapping("/payment")
@Tag(name = "支付接口", description = "多支付方式适配接口")
public class PaymentController {
@Autowired
private PaymentContext paymentContext;
/**
* 处理支付请求
* @param orderId 订单ID
* @param amount 支付金额
* @param payType 支付方式:ALIPAY(支付宝)、WECHAT(微信)、UNIONPAY(银联)
* @return 支付结果
*/
@PostMapping("/process")
@Operation(summary = "处理支付", description = "根据传入的支付方式,调用对应支付策略完成支付")
@Parameters({
@Parameter(name = "orderId", description = "订单ID", required = true, schema = @Schema(type = "string")),
@Parameter(name = "amount", description = "支付金额(大于0)", required = true, schema = @Schema(type = "number", format = "decimal")),
@Parameter(name = "payType", description = "支付方式编码", required = true, schema = @Schema(type = "string", allowableValues = {"ALIPAY", "WECHAT", "UNIONPAY"}))
})
@ApiResponses({
@ApiResponse(responseCode = "200", description = "支付成功", content = @Content(schema = @Schema(type = "string"))),
@ApiResponse(responseCode = "400", description = "参数非法", content = @Content(schema = @Schema(type = "string"))),
@ApiResponse(responseCode = "500", description = "服务器内部错误", content = @Content(schema = @Schema(type = "string")))
})
public String processPayment(
@RequestParam String orderId,
@RequestParam BigDecimal amount,
@RequestParam String payType
) {
log.info("收到支付请求,订单ID:{},金额:{},支付方式:{}", orderId, amount, payType);
return paymentContext.processPayment(orderId, amount, payType);
}
}
2.5 步骤5:配置依赖(pom.xml)
确保项目引入必要的依赖,包括Spring Boot、Lombok、Swagger3等,版本使用最新稳定版。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.jam.demo</groupId>
<artifactId>strategy-pattern-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>strategy-pattern-demo</name>
<description>Demo project for Strategy Pattern in Java</description>
<properties>
<java.version>17</java.version>
<fastjson2.version>2.0.32</fastjson2.version>
<guava.version>32.1.3-jre</guava.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<!-- Swagger3 (SpringDoc OpenAPI) -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.2.0</version>
</dependency>
<!-- FastJSON2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<!-- Guava (集合工具类) -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
2.6 测试验证
启动项目后,访问Swagger3文档地址:http://localhost:8080/swagger-ui/index.html,可通过Swagger界面直接测试接口:
- 测试支付宝支付:orderId=ORDER20240501001,amount=100.00,payType=ALIPAY,返回“支付宝支付成功...”;
- 测试微信支付:orderId=ORDER20240501002,amount=200.00,payType=WECHAT,返回“微信支付成功...”;
- 测试不支持的支付方式:payType=APPLEPAY,将抛出“不支持的支付方式”异常。
2.7 新增策略的扩展方式
如果需要新增“ApplePay支付”,只需新增一个实现PaymentStrategy接口的类,无需修改任何原有代码:
package com.jam.demo.strategy.payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
/**
* ApplePay支付具体策略(新增策略)
* @author ken
*/
@Slf4j
@Component
public class ApplePayStrategy implements PaymentStrategy {
@Override
public String getPayType() {
return "APPLEPAY";
}
@Override
public String pay(String orderId, BigDecimal amount) {
log.info("【ApplePay支付】开始处理订单支付,订单ID:{},支付金额:{}", orderId, amount);
log.info("【ApplePay支付】订单支付成功,订单ID:{}", orderId);
return "ApplePay支付成功,订单ID:" + orderId + ",支付金额:" + amount;
}
}
重启项目后,Spring会自动将ApplePayStrategy注入到paymentStrategyMap中,客户端直接传入payType=APPLEPAY即可调用,完全符合开闭原则。
三、策略模式的进阶实现:会员积分计算场景(结合数据库与MyBatis-Plus)
在实际业务中,策略模式往往需要结合数据库实现动态配置(如会员等级规则存储在数据库)。下面以“会员积分计算”为例,展示策略模式结合MyBatis-Plus、数据库的进阶实现,场景需求如下:
- 不同会员等级(普通会员、白银会员、黄金会员、钻石会员)的积分计算规则不同;
- 积分规则可通过后台配置修改(存储在数据库),无需修改代码;
- 计算积分时,需根据用户的会员等级查询对应的规则,执行计算。
3.1 场景分析与核心设计
3.1.1 积分规则说明
| 会员等级 | 积分计算规则 | 额外奖励积分 |
| 普通会员 | 消费金额 × 1 | 0 |
| 白银会员 | 消费金额 × 1.2 | 消费金额 ≥ 1000 奖励 50 积分 |
| 黄金会员 | 消费金额 × 1.5 | 消费金额 ≥ 800 奖励 80 积分 |
| 钻石会员 | 消费金额 × 2.0 | 消费金额 ≥ 500 奖励 120 积分 |
3.1.2 核心角色设计
- 抽象策略(PointCalculationStrategy):定义积分计算的统一接口;
- 具体策略(普通/白银/黄金/钻石会员积分策略):实现抽象接口,结合数据库规则计算积分;
- 策略上下文(PointCalculationContext):管理策略,提供统一计算入口;
- 数据层(Mapper/Service):通过MyBatis-Plus操作数据库,查询会员等级规则。
3.2 步骤1:数据库设计与初始化(MySQL 8.0)
创建会员等级表(member_level),存储积分规则配置。
3.2.1 建表SQL
CREATE TABLE `member_level` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`level_code` varchar(20) NOT NULL COMMENT '会员等级编码(ORDINARY/SILVER/GOLD/DIAMOND)',
`level_name` varchar(50) NOT NULL COMMENT '会员等级名称',
`point_rate` decimal(5,2) NOT NULL COMMENT '积分倍率(消费金额×倍率)',
`reward_condition` decimal(10,2) DEFAULT NULL COMMENT '额外奖励条件(消费金额≥此值)',
`reward_points` int DEFAULT 0 COMMENT '额外奖励积分',
`is_enable` tinyint NOT NULL DEFAULT 1 COMMENT '是否启用(1-启用,0-禁用)',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_level_code` (`level_code`) COMMENT '会员等级编码唯一'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员等级表(积分规则配置)';
3.2.2 初始化数据
INSERT INTO `member_level` (`level_code`, `level_name`, `point_rate`, `reward_condition`, `reward_points`, `is_enable`)
VALUES
('ORDINARY', '普通会员', 1.00, NULL, 0, 1),
('SILVER', '白银会员', 1.20, 1000.00, 50, 1),
('GOLD', '黄金会员', 1.50, 800.00, 80, 1),
('DIAMOND', '钻石会员', 2.00, 500.00, 120, 1);
3.3 步骤2:引入MyBatis-Plus依赖(pom.xml)
在原有依赖基础上,新增MyBatis-Plus和MySQL驱动依赖:
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
3.4 步骤3:配置数据库连接(application.yml)
spring:
datasource:
url: jdbc:mysql://localhost:3306/strategy_demo?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root123456
driver-class-name: com.mysql.cj.jdbc.Driver
# MyBatis-Plus配置
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.jam.demo.entity
configuration:
map-underscore-to-camel-case: true # 下划线转驼峰
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印SQL日志
global-config:
db-config:
id-type: auto # 主键自增
logic-delete-field: isDelete # 逻辑删除字段
logic-delete-value: 1 # 逻辑删除值(1-删除)
logic-not-delete-value: 0 # 逻辑未删除值(0-未删除)
3.5 步骤4:定义实体类、Mapper、Service(MyBatis-Plus)
3.5.1 实体类(MemberLevel)
package com.jam.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 会员等级表实体类
* @author ken
*/
@Data
@TableName("member_level")
public class MemberLevel {
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 会员等级编码(ORDINARY/SILVER/GOLD/DIAMOND)
*/
private String levelCode;
/**
* 会员等级名称
*/
private String levelName;
/**
* 积分倍率(消费金额×倍率)
*/
private BigDecimal pointRate;
/**
* 额外奖励条件(消费金额≥此值)
*/
private BigDecimal rewardCondition;
/**
* 额外奖励积分
*/
private Integer rewardPoints;
/**
* 是否启用(1-启用,0-禁用)
*/
private Integer isEnable;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
3.5.2 Mapper接口(MemberLevelMapper)
package com.jam.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.MemberLevel;
import org.springframework.stereotype.Repository;
/**
* 会员等级表Mapper
* @author ken
*/
@Repository
public interface MemberLevelMapper extends BaseMapper<MemberLevel> {
}
3.5.3 Service接口与实现类
package com.jam.demo.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.jam.demo.entity.MemberLevel;
/**
* 会员等级服务接口
* @author ken
*/
public interface MemberLevelService extends IService<MemberLevel> {
/**
* 根据会员等级编码查询启用的等级规则
* @param levelCode 会员等级编码
* @return 会员等级规则
*/
MemberLevel getEnableLevelByCode(String levelCode);
}
package com.jam.demo.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jam.demo.entity.MemberLevel;
import com.jam.demo.mapper.MemberLevelMapper;
import com.jam.demo.service.MemberLevelService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
/**
* 会员等级服务实现类
* @author ken
*/
@Slf4j
@Service
public class MemberLevelServiceImpl extends ServiceImpl<MemberLevelMapper, MemberLevel> implements MemberLevelService {
@Override
public MemberLevel getEnableLevelByCode(String levelCode) {
LambdaQueryWrapper<MemberLevel> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(MemberLevel::getLevelCode, levelCode)
.eq(MemberLevel::getIsEnable, 1); // 只查询启用的规则
MemberLevel memberLevel = baseMapper.selectOne(queryWrapper);
if (ObjectUtils.isEmpty(memberLevel)) {
log.error("未查询到启用的会员等级规则,等级编码:{}", levelCode);
throw new IllegalArgumentException("未查询到启用的会员等级规则:" + levelCode);
}
return memberLevel;
}
}
3.6 步骤5:定义抽象策略与具体策略
3.6.1 抽象策略接口(PointCalculationStrategy)
package com.jam.demo.strategy.point;
import com.jam.demo.entity.MemberLevel;
import java.math.BigDecimal;
/**
* 积分计算策略抽象接口
* @author ken
*/
public interface PointCalculationStrategy {
/**
* 获取会员等级编码(对应member_level表的level_code)
* @return 会员等级编码
*/
String getLevelCode();
/**
* 计算积分
* @param consumeAmount 消费金额
* @param memberLevel 会员等级规则(从数据库查询)
* @return 最终积分
*/
Integer calculatePoints(BigDecimal consumeAmount, MemberLevel memberLevel);
}
3.6.2 具体策略实现类
3.6.2.1 普通会员积分策略
package com.jam.demo.strategy.point;
import com.jam.demo.entity.MemberLevel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
/**
* 普通会员积分计算策略
* 规则:消费金额 × 1,无额外奖励
* @author ken
*/
@Slf4j
@Component
public class OrdinaryMemberPointStrategy implements PointCalculationStrategy {
@Override
public String getLevelCode() {
return "ORDINARY";
}
@Override
public Integer calculatePoints(BigDecimal consumeAmount, MemberLevel memberLevel) {
log.info("【普通会员积分计算】开始计算积分,消费金额:{},会员等级规则:{}", consumeAmount, memberLevel);
// 普通会员:消费金额 × 积分倍率(1.00),取整数
BigDecimal pointAmount = consumeAmount.multiply(memberLevel.getPointRate());
Integer points = pointAmount.intValue();
log.info("【普通会员积分计算】计算完成,消费金额:{},积分:{}", consumeAmount, points);
return points;
}
}
3.6.2.2 白银会员积分策略
package com.jam.demo.strategy.point;
import com.jam.demo.entity.MemberLevel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import java.math.BigDecimal;
/**
* 白银会员积分计算策略
* 规则:消费金额 × 1.2,消费金额≥1000奖励50积分
* @author ken
*/
@Slf4j
@Component
public class SilverMemberPointStrategy implements PointCalculationStrategy {
@Override
public String getLevelCode() {
return "SILVER";
}
@Override
public Integer calculatePoints(BigDecimal consumeAmount, MemberLevel memberLevel) {
log.info("【白银会员积分计算】开始计算积分,消费金额:{},会员等级规则:{}", consumeAmount, memberLevel);
// 基础积分:消费金额 × 积分倍率
BigDecimal basePoint = consumeAmount.multiply(memberLevel.getPointRate());
Integer points = basePoint.intValue();
// 额外奖励积分:满足奖励条件则添加
BigDecimal rewardCondition = memberLevel.getRewardCondition();
if (!ObjectUtils.isEmpty(rewardCondition) && consumeAmount.compareTo(rewardCondition) >= 0) {
points += memberLevel.getRewardPoints();
log.info("【白银会员积分计算】满足额外奖励条件(消费≥{}),添加奖励积分:{}", rewardCondition, memberLevel.getRewardPoints());
}
log.info("【白银会员积分计算】计算完成,消费金额:{},最终积分:{}", consumeAmount, points);
return points;
}
}
3.6.2.3 黄金会员积分策略
package com.jam.demo.strategy.point;
import com.jam.demo.entity.MemberLevel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import java.math.BigDecimal;
/**
* 黄金会员积分计算策略
* 规则:消费金额 × 1.5,消费金额≥800奖励80积分
* @author ken
*/
@Slf4j
@Component
public class GoldMemberPointStrategy implements PointCalculationStrategy {
@Override
public String getLevelCode() {
return "GOLD";
}
@Override
public Integer calculatePoints(BigDecimal consumeAmount, MemberLevel memberLevel) {
log.info("【黄金会员积分计算】开始计算积分,消费金额:{},会员等级规则:{}", consumeAmount, memberLevel);
// 基础积分
BigDecimal basePoint = consumeAmount.multiply(memberLevel.getPointRate());
Integer points = basePoint.intValue();
// 额外奖励积分
BigDecimal rewardCondition = memberLevel.getRewardCondition();
if (!ObjectUtils.isEmpty(rewardCondition) && consumeAmount.compareTo(rewardCondition) >= 0) {
points += memberLevel.getRewardPoints();
log.info("【黄金会员积分计算】满足额外奖励条件(消费≥{}),添加奖励积分:{}", rewardCondition, memberLevel.getRewardPoints());
}
log.info("【黄金会员积分计算】计算完成,消费金额:{},最终积分:{}", consumeAmount, points);
return points;
}
}
3.6.2.4 钻石会员积分策略
package com.jam.demo.strategy.point;
import com.jam.demo.entity.MemberLevel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import java.math.BigDecimal;
/**
* 钻石会员积分计算策略
* 规则:消费金额 × 2.0,消费金额≥500奖励120积分
* @author ken
*/
@Slf4j
@Component
public class DiamondMemberPointStrategy implements PointCalculationStrategy {
@Override
public String getLevelCode() {
return "DIAMOND";
}
@Override
public Integer calculatePoints(BigDecimal consumeAmount, MemberLevel memberLevel) {
log.info("【钻石会员积分计算】开始计算积分,消费金额:{},会员等级规则:{}", consumeAmount, memberLevel);
// 基础积分
BigDecimal basePoint = consumeAmount.multiply(memberLevel.getPointRate());
Integer points = basePoint.intValue();
// 额外奖励积分
BigDecimal rewardCondition = memberLevel.getRewardCondition();
if (!ObjectUtils.isEmpty(rewardCondition) && consumeAmount.compareTo(rewardCondition) >= 0) {
points += memberLevel.getRewardPoints();
log.info("【钻石会员积分计算】满足额外奖励条件(消费≥{}),添加奖励积分:{}", rewardCondition, memberLevel.getRewardPoints());
}
log.info("【钻石会员积分计算】计算完成,消费金额:{},最终积分:{}", consumeAmount, points);
return points;
}
}
3.7 步骤6:实现策略上下文(PointCalculationContext)
package com.jam.demo.strategy.point;
import com.jam.demo.entity.MemberLevel;
import com.jam.demo.service.MemberLevelService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 积分计算策略上下文
* @author ken
*/
@Slf4j
@Component
public class PointCalculationContext {
/**
* 存储所有积分计算策略
*/
private final Map<String, PointCalculationStrategy> pointStrategyMap;
/**
* 会员等级服务(查询数据库规则)
*/
private final MemberLevelService memberLevelService;
/**
* 构造方法注入策略和服务
* @param strategies 所有积分计算策略实现类
* @param memberLevelService 会员等级服务
*/
@Autowired
public PointCalculationContext(Map<String, PointCalculationStrategy> strategies, MemberLevelService memberLevelService) {
this.pointStrategyMap = new ConcurrentHashMap<>(strategies);
this.memberLevelService = memberLevelService;
log.info("初始化积分计算策略Map,加载策略数量:{},策略详情:{}", strategies.size(), strategies.keySet());
}
/**
* 统一积分计算入口
* @param userId 用户ID(用于日志追踪)
* @param consumeAmount 消费金额
* @param levelCode 会员等级编码
* @return 最终积分
*/
public Integer calculatePoints(String userId, BigDecimal consumeAmount, String levelCode) {
// 参数校验
if (!StringUtils.hasText(userId)) {
throw new IllegalArgumentException("用户ID不能为空");
}
if (ObjectUtils.isEmpty(consumeAmount) || consumeAmount.compareTo(BigDecimal.ZERO) <= 0) {
log.error("消费金额非法,用户ID:{},金额:{}", userId, consumeAmount);
throw new IllegalArgumentException("消费金额必须大于0");
}
if (!StringUtils.hasText(levelCode)) {
throw new IllegalArgumentException("会员等级编码不能为空");
}
// 1. 根据等级编码获取对应的积分策略
PointCalculationStrategy strategy = pointStrategyMap.get(levelCode);
if (ObjectUtils.isEmpty(strategy)) {
log.error("不支持的会员等级积分策略,用户ID:{},等级编码:{}", userId, levelCode);
throw new UnsupportedOperationException("不支持的会员等级:" + levelCode);
}
// 2. 从数据库查询该等级的启用规则
MemberLevel memberLevel = memberLevelService.getEnableLevelByCode(levelCode);
// 3. 调用策略计算积分
return strategy.calculatePoints(consumeAmount, memberLevel);
}
}
3.8 步骤7:客户端调用(Controller层)
package com.jam.demo.controller;
import com.jam.demo.strategy.point.PointCalculationContext;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
/**
* 积分计算控制器
* @author ken
*/
@Slf4j
@RestController
@RequestMapping("/points")
@Tag(name = "积分计算接口", description = "不同会员等级的积分计算接口")
public class PointCalculationController {
@Autowired
private PointCalculationContext pointCalculationContext;
/**
* 计算会员消费积分
* @param userId 用户ID
* @param consumeAmount 消费金额
* @param levelCode 会员等级编码:ORDINARY(普通)、SILVER(白银)、GOLD(黄金)、DIAMOND(钻石)
* @return 最终积分
*/
@PostMapping("/calculate")
@Operation(summary = "计算消费积分", description = "根据用户会员等级和消费金额,计算最终积分")
@Parameters({
@Parameter(name = "userId", description = "用户ID", required = true, schema = @Schema(type = "string")),
@Parameter(name = "consumeAmount", description = "消费金额(大于0)", required = true, schema = @Schema(type = "number", format = "decimal")),
@Parameter(name = "levelCode", description = "会员等级编码", required = true, schema = @Schema(type = "string", allowableValues = {"ORDINARY", "SILVER", "GOLD", "DIAMOND"}))
})
@ApiResponses({
@ApiResponse(responseCode = "200", description = "计算成功", content = @Content(schema = @Schema(type = "integer"))),
@ApiResponse(responseCode = "400", description = "参数非法或规则不存在", content = @Content(schema = @Schema(type = "string"))),
@ApiResponse(responseCode = "500", description = "服务器内部错误", content = @Content(schema = @Schema(type = "string")))
})
public Integer calculatePoints(
@RequestParam String userId,
@RequestParam BigDecimal consumeAmount,
@RequestParam String levelCode
) {
log.info("收到积分计算请求,用户ID:{},消费金额:{},会员等级:{}", userId, consumeAmount, levelCode);
return pointCalculationContext.calculatePoints(userId, consumeAmount, levelCode);
}
}
3.9 测试验证
- 普通会员消费800元:userId=USER001,consumeAmount=800.00,levelCode=ORDINARY → 积分=800×1=800;
- 白银会员消费1200元:userId=USER002,consumeAmount=1200.00,levelCode=SILVER → 基础积分=1200×1.2=1440,额外奖励50 → 总积分1490;
- 钻石会员消费400元:userId=USER003,consumeAmount=400.00,levelCode=DIAMOND → 基础积分=400×2.0=800,未满足500元奖励条件 → 总积分800;
- 黄金会员消费800元:userId=USER004,consumeAmount=800.00,levelCode=GOLD → 基础积分=800×1.5=1200,满足800元奖励条件 → 总积分1280。
3.10 动态调整规则(无需修改代码)
如果需要调整白银会员的积分规则(比如将倍率改为1.3,奖励条件改为≥800元奖励60积分),只需直接修改数据库表member_level中level_code=SILVER的记录:
UPDATE `member_level`
SET `point_rate` = 1.30, `reward_condition` = 800.00, `reward_points` = 60
WHERE `level_code` = 'SILVER';
修改后无需重启项目,再次调用接口时,策略会自动使用更新后的规则计算积分,实现了规则的动态配置。
四、策略模式的高级实现:动态规则校验场景(结合工厂模式+策略模式)
在更复杂的业务场景中,策略模式常与工厂模式结合使用,进一步优化策略的创建和管理。下面以“动态规则校验”为例,展示策略模式+工厂模式的高级实现,场景需求如下:
- 不同类型的订单(实物订单、虚拟订单、跨境订单)需要执行不同的校验规则(如实物订单校验物流地址,虚拟订单校验有效期,跨境订单校验关税信息);
- 校验规则可能动态增减,需要支持灵活扩展;
- 客户端传入订单类型,自动执行对应的校验逻辑,返回校验结果。
4.1 场景分析与核心设计
4.1.1 校验规则说明
| 订单类型 | 校验规则 |
| 实物订单(PHYSICAL) | 1. 物流地址不能为空;2. 物流地址格式正确;3. 商品库存充足 |
| 虚拟订单(VIRTUAL) | 1. 订单有效期未过期;2. 虚拟商品激活码存在;3. 用户未重复购买 |
| 跨境订单(CROSS_BORDER) | 1. 关税信息完整;2. 收件人身份证信息完整;3. 商品符合跨境限购规则 |
4.1.2 核心设计思路
- 策略模式:封装不同订单类型的校验规则(具体策略);
- 工厂模式:创建策略工厂,负责根据订单类型创建对应的策略对象,替代上下文直接管理策略的方式,进一步解耦;
- 统一结果封装:定义校验结果对象,规范返回格式;
- 异常处理:自定义校验异常,统一处理校验失败场景。
4.2 步骤1:定义统一结果对象与自定义异常
4.2.1 校验结果对象(ValidationResult)
package com.jam.demo.vo;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 规则校验结果VO
* @author ken
*/
@Data
@Accessors(chain = true)
public class ValidationResult {
/**
* 校验是否通过
*/
private boolean pass;
/**
* 校验失败信息(pass=false时非空)
*/
private String errorMsg;
/**
* 订单ID
*/
private String orderId;
/**
* 订单类型
*/
private String orderType;
/**
* 成功结果构造方法
* @param orderId 订单ID
* @param orderType 订单类型
* @return 校验结果
*/
public static ValidationResult success(String orderId, String orderType) {
return new ValidationResult()
.setPass(true)
.setOrderId(orderId)
.setOrderType(orderType);
}
/**
* 失败结果构造方法
* @param orderId 订单ID
* @param orderType 订单类型
* @param errorMsg 失败信息
* @return 校验结果
*/
public static ValidationResult fail(String orderId, String orderType, String errorMsg) {
return new ValidationResult()
.setPass(false)
.setOrderId(orderId)
.setOrderType(orderType)
.setErrorMsg(errorMsg);
}
}
4.2.2 自定义校验异常(ValidationException)
package com.jam.demo.exception;
import lombok.Getter;
/**
* 订单校验异常
* @author ken
*/
@Getter
public class ValidationException extends RuntimeException {
/**
* 订单ID
*/
private final String orderId;
/**
* 订单类型
*/
private final String orderType;
public ValidationException(String message, String orderId, String orderType) {
super(message);
this.orderId = orderId;
this.orderType = orderType;
}
public ValidationException(String message, Throwable cause, String orderId, String orderType) {
super(message, cause);
this.orderId = orderId;
this.orderType = orderType;
}
}
4.3 步骤2:定义订单实体与抽象策略
4.3.1 订单实体(OrderDTO)
package com.jam.demo.dto;
import lombok.Data;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
import java.math.BigDecimal;
/**
* 订单DTO(数据传输对象)
* @author ken
*/
@Data
@Accessors(chain = true)
public class OrderDTO {
/**
* 订单ID
*/
private String orderId;
/**
* 订单类型:PHYSICAL(实物订单)、VIRTUAL(虚拟订单)、CROSS_BORDER(跨境订单)
*/
private String orderType;
/**
* 物流地址(实物订单必填)
*/
private String logisticsAddress;
/**
* 订单有效期(虚拟订单必填)
*/
private LocalDateTime validTime;
/**
* 虚拟商品激活码(虚拟订单必填)
*/
private String activationCode;
/**
* 关税信息(跨境订单必填)
*/
private String tariffInfo;
/**
* 收件人身份证号(跨境订单必填)
*/
private String idCard;
/**
* 商品ID
*/
private String productId;
/**
* 购买数量
*/
private Integer quantity;
/**
* 商品单价
*/
private BigDecimal price;
}
4.3.2 抽象校验策略(OrderValidationStrategy)
package com.jam.demo.strategy.validation;
import com.jam.demo.dto.OrderDTO;
import com.jam.demo.vo.ValidationResult;
/**
* 订单校验策略抽象接口
* @author ken
*/
public interface OrderValidationStrategy {
/**
* 获取订单类型编码(对应OrderDTO的orderType)
* @return 订单类型编码
*/
String getOrderType();
/**
* 执行订单校验
* @param orderDTO 订单数据
* @return 校验结果
*/
ValidationResult validate(OrderDTO orderDTO);
}
4.4 步骤3:实现具体校验策略
4.4.1 实物订单校验策略
package com.jam.demo.strategy.validation;
import com.jam.demo.dto.OrderDTO;
import com.jam.demo.vo.ValidationResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
/**
* 实物订单校验策略
* 校验规则:1.物流地址不能为空;2.物流地址格式正确;3.商品库存充足
* @author ken
*/
@Slf4j
@Component
public class PhysicalOrderValidationStrategy implements OrderValidationStrategy {
@Override
public String getOrderType() {
return "PHYSICAL";
}
@Override
public ValidationResult validate(OrderDTO orderDTO) {
log.info("【实物订单校验】开始校验订单,订单ID:{},订单信息:{}", orderDTO.getOrderId(), orderDTO);
String orderId = orderDTO.getOrderId();
String orderType = orderDTO.getOrderType();
// 1. 校验物流地址不能为空
if (!StringUtils.hasText(orderDTO.getLogisticsAddress())) {
log.error("【实物订单校验】失败,物流地址不能为空,订单ID:{}", orderId);
return ValidationResult.fail(orderId, orderType, "物流地址不能为空");
}
// 2. 校验物流地址格式(简单模拟:需包含省、市、区、详细地址)
String logisticsAddress = orderDTO.getLogisticsAddress();
if (!(logisticsAddress.contains("省") && logisticsAddress.contains("市") && logisticsAddress.contains("区") && logisticsAddress.length() > 10)) {
log.error("【实物订单校验】失败,物流地址格式不正确,订单ID:{},地址:{}", orderId, logisticsAddress);
return ValidationResult.fail(orderId, orderType, "物流地址格式不正确,需包含省、市、区及详细地址");
}
// 3. 校验商品库存充足(模拟调用库存服务,此处简化判断)
boolean stockSufficient = checkStock(orderDTO.getProductId(), orderDTO.getQuantity());
if (!stockSufficient) {
log.error("【实物订单校验】失败,商品库存不足,订单ID:{},商品ID:{},所需数量:{}",
orderId, orderDTO.getProductId(), orderDTO.getQuantity());
return ValidationResult.fail(orderId, orderType, "商品库存不足");
}
log.info("【实物订单校验】成功,订单ID:{}", orderId);
return ValidationResult.success(orderId, orderType);
}
/**
* 模拟校验商品库存
* @param productId 商品ID
* @param quantity 所需数量
* @return 库存是否充足
*/
private boolean checkStock(String productId, Integer quantity) {
// 实际场景中需调用库存服务查询真实库存,此处简化为:商品ID非空且数量≤100则库存充足
return StringUtils.hasText(productId) && quantity != null && quantity <= 100;
}
}
4.4.2 虚拟订单校验策略
package com.jam.demo.strategy.validation;
import com.jam.demo.dto.OrderDTO;
import com.jam.demo.vo.ValidationResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
/**
* 虚拟订单校验策略
* 校验规则:1.订单有效期未过期;2.虚拟商品激活码存在;3.用户未重复购买
* @author ken
*/
@Slf4j
@Component
public class VirtualOrderValidationStrategy implements OrderValidationStrategy {
@Override
public String getOrderType() {
return "VIRTUAL";
}
@Override
public ValidationResult validate(OrderDTO orderDTO) {
log.info("【虚拟订单校验】开始校验订单,订单ID:{},订单信息:{}", orderDTO.getOrderId(), orderDTO);
String orderId = orderDTO.getOrderId();
String orderType = orderDTO.getOrderType();
// 1. 校验订单有效期未过期
LocalDateTime validTime = orderDTO.getValidTime();
if (ObjectUtils.isEmpty(validTime)) {
log.error("【虚拟订单校验】失败,订单有效期不能为空,订单ID:{}", orderId);
return ValidationResult.fail(orderId, orderType, "订单有效期不能为空");
}
if (validTime.isBefore(LocalDateTime.now())) {
log.error("【虚拟订单校验】失败,订单已过期,订单ID:{},有效期:{}", orderId, validTime);
return ValidationResult.fail(orderId, orderType, "订单已过期,有效期:" + validTime);
}
// 2. 校验虚拟商品激活码存在
String activationCode = orderDTO.getActivationCode();
if (!StringUtils.hasText(activationCode)) {
log.error("【虚拟订单校验】失败,虚拟商品激活码不能为空,订单ID:{}", orderId);
return ValidationResult.fail(orderId, orderType, "虚拟商品激活码不能为空");
}
boolean activationCodeExists = checkActivationCode(activationCode);
if (!activationCodeExists) {
log.error("【虚拟订单校验】失败,虚拟商品激活码不存在,订单ID:{},激活码:{}", orderId, activationCode);
return ValidationResult.fail(orderId, orderType, "虚拟商品激活码不存在");
}
// 3. 校验用户未重复购买(模拟调用用户购买记录服务)
boolean repeatedPurchase = checkRepeatedPurchase(orderDTO.getProductId());
if (repeatedPurchase) {
log.error("【虚拟订单校验】失败,用户已重复购买该虚拟商品,订单ID:{},商品ID:{}", orderId, orderDTO.getProductId());
return ValidationResult.fail(orderId, orderType, "用户已重复购买该虚拟商品");
}
log.info("【虚拟订单校验】成功,订单ID:{}", orderId);
return ValidationResult.success(orderId, orderType);
}
/**
* 模拟校验激活码存在
* @param activationCode 激活码
* @return 激活码是否存在
*/
private boolean checkActivationCode(String activationCode) {
// 实际场景中需查询激活码数据库,此处简化为:激活码长度≥8则存在
return activationCode.length() >= 8;
}
/**
* 模拟校验用户重复购买
* @param productId 商品ID
* @return 是否重复购买
*/
private boolean checkRepeatedPurchase(String productId) {
// 实际场景中需查询用户购买记录,此处简化为:商品ID为"VIRTUAL001"则视为重复购买
return "VIRTUAL001".equals(productId);
}
}
4.4.3 跨境订单校验策略
package com.jam.demo.strategy.validation;
import com.jam.demo.dto.OrderDTO;
import com.jam.demo.vo.ValidationResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.regex.Pattern;
/**
* 跨境订单校验策略
* 校验规则:1.关税信息完整;2.收件人身份证信息完整;3.商品符合跨境限购规则
* @author ken
*/
@Slf4j
@Component
public class CrossBorderOrderValidationStrategy implements OrderValidationStrategy {
/**
* 身份证号正则表达式(简单匹配18位)
*/
private static final Pattern ID_CARD_PATTERN = Pattern.compile("^[1-9]\\d{5}(18|19|20)\\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$");
@Override
public String getOrderType() {
return "CROSS_BORDER";
}
@Override
public ValidationResult validate(OrderDTO orderDTO) {
log.info("【跨境订单校验】开始校验订单,订单ID:{},订单信息:{}", orderDTO.getOrderId(), orderDTO);
String orderId = orderDTO.getOrderId();
String orderType = orderDTO.getOrderType();
// 1. 校验关税信息完整
String tariffInfo = orderDTO.getTariffInfo();
if (!StringUtils.hasText(tariffInfo)) {
log.error("【跨境订单校验】失败,关税信息不能为空,订单ID:{}", orderId);
return ValidationResult.fail(orderId, orderType, "关税信息不能为空");
}
if (!tariffInfo.contains("关税金额") || !tariffInfo.contains("税率") || !tariffInfo.contains("申报价值")) {
log.error("【跨境订单校验】失败,关税信息不完整,订单ID:{},关税信息:{}", orderId, tariffInfo);
return ValidationResult.fail(orderId, orderType, "关税信息不完整,需包含关税金额、税率、申报价值");
}
// 2. 校验收件人身份证信息完整
String idCard = orderDTO.getIdCard();
if (!StringUtils.hasText(idCard)) {
log.error("【跨境订单校验】失败,收件人身份证号不能为空,订单ID:{}", orderId);
return ValidationResult.fail(orderId, orderType, "收件人身份证号不能为空");
}
if (!ID_CARD_PATTERN.matcher(idCard).matches()) {
log.error("【跨境订单校验】失败,收件人身份证号格式不正确,订单ID:{},身份证号:{}", orderId, idCard);
return ValidationResult.fail(orderId, orderType, "收件人身份证号格式不正确");
}
// 3. 校验商品符合跨境限购规则(模拟:单个商品购买数量≤5,订单总金额≤5000元)
Integer quantity = orderDTO.getQuantity();
BigDecimal price = orderDTO.getPrice();
if (quantity == null || quantity > 5) {
log.error("【跨境订单校验】失败,商品超出限购数量(≤5),订单ID:{},购买数量:{}", orderId, quantity);
return ValidationResult.fail(orderId, orderType, "商品超出限购数量,单个商品最多购买5件");
}
if (ObjectUtils.isEmpty(price)) {
log.error("【跨境订单校验】失败,商品单价不能为空,订单ID:{}", orderId);
return ValidationResult.fail(orderId, orderType, "商品单价不能为空");
}
BigDecimal totalAmount = price.multiply(new BigDecimal(quantity));
if (totalAmount.compareTo(new BigDecimal(5000)) > 0) {
log.error("【跨境订单校验】失败,订单总金额超出跨境限购额度(≤5000元),订单ID:{},总金额:{}", orderId, totalAmount);
return ValidationResult.fail(orderId, orderType, "订单总金额超出跨境限购额度,最多5000元");
}
log.info("【跨境订单校验】成功,订单ID:{}", orderId);
return ValidationResult.success(orderId, orderType);
}
}
4.5 步骤4:实现策略工厂(OrderValidationStrategyFactory)
策略工厂负责管理所有具体策略,根据订单类型创建并返回对应的策略对象。此处结合Spring的Bean工厂特性,在工厂初始化时自动加载所有OrderValidationStrategy实现类,存入Map中供后续获取。
package com.jam.demo.strategy.factory;
import com.jam.demo.strategy.validation.OrderValidationStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 订单校验策略工厂
* 负责根据订单类型获取对应的校验策略
* @author ken
*/
@Slf4j
@Component
public class OrderValidationStrategyFactory implements InitializingBean {
/**
* 存储所有订单校验策略,key:订单类型编码,value:对应策略对象
*/
private final Map<String, OrderValidationStrategy> strategyMap = new ConcurrentHashMap<>();
/**
* 自动注入所有OrderValidationStrategy实现类
*/
@Autowired
private Map<String, OrderValidationStrategy> strategyBeans;
/**
* 根据订单类型获取策略
* @param orderType 订单类型编码
* @return 对应的校验策略
*/
public OrderValidationStrategy getStrategy(String orderType) {
OrderValidationStrategy strategy = strategyMap.get(orderType);
if (ObjectUtils.isEmpty(strategy)) {
log.error("不支持的订单类型校验策略,订单类型:{}", orderType);
throw new UnsupportedOperationException("不支持的订单类型:" + orderType);
}
return strategy;
}
/**
* InitializingBean接口方法,Bean初始化后执行
* 将注入的策略Bean按订单类型编码存入strategyMap
*/
@Override
public void afterPropertiesSet() throws Exception {
// 遍历所有策略Bean,将key设置为策略的orderType,覆盖默认的Bean名称
for (OrderValidationStrategy strategy : strategyBeans.values()) {
String orderType = strategy.getOrderType();
strategyMap.put(orderType, strategy);
log.info("订单校验策略工厂加载策略:订单类型={},策略类={}", orderType, strategy.getClass().getName());
}
log.info("订单校验策略工厂初始化完成,共加载策略数量:{}", strategyMap.size());
}
}
4.6 步骤5:实现服务层(OrderValidationService)
服务层作为客户端与工厂/策略的中间层,负责接收客户端请求,通过工厂获取策略,执行校验逻辑,并处理异常。
package com.jam.demo.service;
import com.jam.demo.dto.OrderDTO;
import com.jam.demo.exception.ValidationException;
import com.jam.demo.strategy.factory.OrderValidationStrategyFactory;
import com.jam.demo.strategy.validation.OrderValidationStrategy;
import com.jam.demo.vo.ValidationResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* 订单校验服务
* @author ken
*/
@Slf4j
@Service
public class OrderValidationService {
@Autowired
private OrderValidationStrategyFactory strategyFactory;
/**
* 执行订单校验
* @param orderDTO 订单数据
* @return 校验结果
*/
@Transactional(rollbackFor = Exception.class)
public ValidationResult validateOrder(OrderDTO orderDTO) {
// 基础参数校验
if (ObjectUtils.isEmpty(orderDTO)) {
log.error("订单校验失败,订单数据不能为空");
throw new IllegalArgumentException("订单数据不能为空");
}
if (!StringUtils.hasText(orderDTO.getOrderId())) {
log.error("订单校验失败,订单ID不能为空");
throw new IllegalArgumentException("订单ID不能为空");
}
if (!StringUtils.hasText(orderDTO.getOrderType())) {
log.error("订单校验失败,订单类型不能为空,订单ID:{}", orderDTO.getOrderId());
throw new IllegalArgumentException("订单类型不能为空");
}
try {
// 1. 通过工厂获取对应策略
OrderValidationStrategy strategy = strategyFactory.getStrategy(orderDTO.getOrderType());
// 2. 执行校验
return strategy.validate(orderDTO);
} catch (UnsupportedOperationException e) {
log.error("订单校验失败,订单ID:{},订单类型:{}", orderDTO.getOrderId(), orderDTO.getOrderType(), e);
return ValidationResult.fail(orderDTO.getOrderId(), orderDTO.getOrderType(), e.getMessage());
} catch (Exception e) {
log.error("订单校验异常,订单ID:{},订单类型:{}", orderDTO.getOrderId(), orderDTO.getOrderType(), e);
// 抛出自定义异常,便于全局异常处理
throw new ValidationException("订单校验异常:" + e.getMessage(), e, orderDTO.getOrderId(), orderDTO.getOrderType());
}
}
}
4.7 步骤6:客户端调用(Controller层)
package com.jam.demo.controller;
import com.jam.demo.dto.OrderDTO;
import com.jam.demo.service.OrderValidationService;
import com.jam.demo.vo.ValidationResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 订单校验控制器
* @author ken
*/
@Slf4j
@RestController
@RequestMapping("/order/validation")
@Tag(name = "订单校验接口", description = "不同类型订单的动态规则校验接口")
public class OrderValidationController {
@Autowired
private OrderValidationService orderValidationService;
/**
* 执行订单校验
* @param orderDTO 订单数据
* @return 校验结果
*/
@PostMapping
@Operation(summary = "执行订单校验", description = "根据订单类型自动匹配对应的校验策略,执行规则校验")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "校验完成(成功/失败)", content = @Content(schema = @Schema(implementation = ValidationResult.class))),
@ApiResponse(responseCode = "400", description = "参数非法", content = @Content(schema = @Schema(type = "string"))),
@ApiResponse(responseCode = "500", description = "服务器内部错误", content = @Content(schema = @Schema(type = "string")))
})
public ValidationResult validateOrder(@RequestBody OrderDTO orderDTO) {
log.info("收到订单校验请求,订单ID:{},订单类型:{}", orderDTO.getOrderId(), orderDTO.getOrderType());
return orderValidationService.validateOrder(orderDTO);
}
}
4.8 步骤7:全局异常处理(GlobalExceptionHandler)
统一处理校验过程中抛出的异常,返回规范的错误响应。
package com.jam.demo.exception;
import com.jam.demo.vo.ValidationResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理器
* @author ken
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理订单校验异常
* @param e 校验异常
* @return 校验失败结果
*/
@ExceptionHandler(ValidationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ValidationResult handleValidationException(ValidationException e) {
log.error("订单校验异常:{},订单ID:{},订单类型:{}", e.getMessage(), e.getOrderId(), e.getOrderType(), e);
return ValidationResult.fail(e.getOrderId(), e.getOrderType(), e.getMessage());
}
/**
* 处理参数非法异常
* @param e 参数异常
* @return 校验失败结果
*/
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ValidationResult handleIllegalArgumentException(IllegalArgumentException e) {
log.error("参数非法:{}", e.getMessage(), e);
return ValidationResult.fail(null, null, e.getMessage());
}
/**
* 处理不支持的操作异常
* @param e 不支持操作异常
* @return 校验失败结果
*/
@ExceptionHandler(UnsupportedOperationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ValidationResult handleUnsupportedOperationException(UnsupportedOperationException e) {
log.error("不支持的操作:{}", e.getMessage(), e);
return ValidationResult.fail(null, null, e.getMessage());
}
/**
* 处理通用异常
* @param e 通用异常
* @return 校验失败结果
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ValidationResult handleException(Exception e) {
log.error("服务器内部错误:{}", e.getMessage(), e);
return ValidationResult.fail(null, null, "服务器内部错误:" + e.getMessage());
}
}
4.9 测试验证
通过Postman或Swagger3调用/order/validation接口,传入不同类型的订单数据,验证校验逻辑:
4.9.1 测试1:实物订单(物流地址正确,库存充足)
请求体:
{
"orderId": "PHYSICAL20240501001",
"orderType": "PHYSICAL",
"logisticsAddress": "广东省深圳市南山区科技园XX路XX号",
"productId": "PHYSICAL001",
"quantity": 2,
"price": 100.00
}
响应结果:
{
"pass": true,
"errorMsg": null,
"orderId": "PHYSICAL20240501001",
"orderType": "PHYSICAL"
}
4.9.2 测试2:虚拟订单(已过期)
请求体:
{
"orderId": "VIRTUAL20240501001",
"orderType": "VIRTUAL",
"validTime": "2023-01-01T12:00:00",
"activationCode": "VCODE12345678",
"productId": "VIRTUAL002"
}
响应结果:
{
"pass": false,
"errorMsg": "订单已过期,有效期:2023-01-01T12:00",
"orderId": "VIRTUAL20240501001",
"orderType": "VIRTUAL"
}
4.9.3 测试3:跨境订单(总金额超出限购)
请求体:
{
"orderId": "CROSS20240501001",
"orderType": "CROSS_BORDER",
"tariffInfo": "关税金额:100元,税率:10%,申报价值:6000元",
"idCard": "440301199001011234",
"productId": "CROSS001",
"quantity": 2,
"price": 3000.00
}
响应结果:
{
"pass": false,
"errorMsg": "订单总金额超出跨境限购额度,最多5000元",
"orderId": "CROSS20240501001",
"orderType": "CROSS_BORDER"
}
4.9.4 测试4:不支持的订单类型
请求体:
{
"orderId": "TEST20240501001",
"orderType": "TEST",
"logisticsAddress": "广东省深圳市南山区"
}
响应结果:
{
"pass": false,
"errorMsg": "不支持的订单类型:TEST",
"orderId": "TEST20240501001",
"orderType": "TEST"
}
4.10 扩展新订单类型校验策略
若需新增“团购订单(GROUP_BUY)”的校验策略,只需新增实现OrderValidationStrategy接口的类,无需修改原有任何代码:
package com.jam.demo.strategy.validation;
import com.jam.demo.dto.OrderDTO;
import com.jam.demo.vo.ValidationResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
/**
* 团购订单校验策略(新增策略)
* 校验规则:1. 团购人数≥2;2. 团购截止时间未过期
* @author ken
*/
@Slf4j
@Component
public class GroupBuyOrderValidationStrategy implements OrderValidationStrategy {
@Override
public String getOrderType() {
return "GROUP_BUY";
}
@Override
public ValidationResult validate(OrderDTO orderDTO) {
log.info("【团购订单校验】开始校验订单,订单ID:{},订单信息:{}", orderDTO.getOrderId(), orderDTO);
String orderId = orderDTO.getOrderId();
String orderType = orderDTO.getOrderType();
// 此处省略具体校验逻辑(团购人数、截止时间等)
log.info("【团购订单校验】成功,订单ID:{}", orderId);
return ValidationResult.success(orderId, orderType);
}
}
重启项目后,策略工厂会自动加载该策略,客户端传入orderType=GROUP_BUY即可执行团购订单的校验,扩展极为灵活。
五、策略模式的核心优势与适用场景
5.1 核心优势
- 彻底解决if-else臃肿问题:将多分支逻辑封装为独立策略类,代码结构清晰,可读性、可维护性大幅提升;
- 符合开闭原则:新增策略时无需修改原有代码,只需新增策略类并注册,降低代码变更风险;
- 高内聚低耦合:每个策略类专注于自身的算法逻辑,职责单一,策略之间相互独立,降低耦合度;
- 灵活性高:支持策略的动态切换(通过上下文或工厂获取不同策略),可根据业务场景动态调整执行逻辑;
- 便于测试:每个策略类可独立进行单元测试,测试用例更简洁,覆盖度更高。
5.2 适用场景
- 多分支逻辑场景:如多支付方式、多会员等级、多订单类型、多数据校验规则等;
- 算法需要动态切换场景:如根据不同场景切换排序算法、加密算法、校验规则等;
- 算法逻辑复杂且易扩展场景:如复杂的业务规则计算(积分、返利、计费等),规则可能频繁调整或新增;
- 希望降低代码耦合度场景:避免核心业务逻辑与分支判断逻辑强耦合,提升代码复用性。
六、策略模式的常见误区与避坑指南
6.1 常见误区
- 过度设计:简单的少量分支逻辑(如2-3个分支)使用策略模式,反而增加代码复杂度;
- 策略选择逻辑冗余:在客户端重复编写策略选择逻辑,未通过上下文或工厂统一管理;
- 策略对象创建频繁:未对策略对象进行缓存(如Spring管理的单例Bean),导致频繁创建销毁对象,影响性能;
- 忽略策略间的依赖关系:多个策略存在依赖时,未合理处理,导致逻辑混乱;
- 未处理空策略场景:未考虑不支持的策略类型,未抛出明确异常或返回友好提示。
6.2 避坑指南
- 按需使用:分支数量少(≤3)且逻辑简单时,可保留if-else;分支数量多(≥4)或逻辑复杂时,再使用策略模式;
- 统一策略管理:通过上下文或工厂集中管理策略选择逻辑,客户端仅需传入策略类型,无需关注选择过程;
- 复用策略对象:使用Spring管理策略Bean(单例),或通过工厂缓存策略对象,避免频繁创建;
- 明确策略职责:每个策略职责单一,避免策略间的直接依赖,若需依赖可通过服务注入解决;
- 完善异常处理:对不支持的策略类型、参数非法等场景,抛出明确的自定义异常,便于问题定位;
- 结合其他模式优化:复杂场景可结合工厂模式(优化策略创建)、单例模式(复用策略对象)、享元模式(缓存策略)等。
七、策略模式与其他相似模式的区别
7.1 策略模式 vs 工厂模式
- 核心目的不同:策略模式专注于“算法的封装与切换”,解决的是“如何执行不同逻辑”的问题;工厂模式专注于“对象的创建”,解决的是“如何创建不同对象”的问题;
- 角色定位不同:策略模式的核心是“策略接口+具体策略”,客户端通过上下文调用策略的方法;工厂模式的核心是“工厂类+产品类”,客户端通过工厂获取产品对象,再调用产品方法;
- 使用场景互补:实际开发中常结合使用(如本文第四节),工厂模式负责创建策略对象,策略模式负责执行具体算法,进一步解耦创建与执行逻辑。
7.2 策略模式 vs 状态模式
- 核心逻辑不同:策略模式中,策略的切换由客户端主动指定(通过传入策略类型);状态模式中,状态的切换由对象的内部状态决定,客户端无需主动干预;
- 关注点不同:策略模式关注“不同算法的替换”,策略之间相互独立,无依赖;状态模式关注“对象状态的变化及状态对应的行为”,状态之间可能存在依赖(如状态流转);
- 适用场景不同:策略模式适用于客户端主动选择算法的场景;状态模式适用于对象状态随业务流程自动变化的场景(如订单状态:待支付→已支付→待发货→已发货)。
7.3 策略模式 vs 模板方法模式
- 核心思想不同:策略模式是“封装不同的算法”,算法的整体逻辑可能完全不同;模板方法模式是“封装固定流程,允许子类替换流程中的特定步骤”,整体流程固定,仅局部步骤可定制;
- 实现方式不同:策略模式通过接口定义统一方法,具体策略实现接口;模板方法模式通过抽象类定义固定流程(模板方法),子类继承抽象类并实现抽象方法(可定制步骤);
- 耦合度不同:策略模式中客户端与策略接口耦合,耦合度低;模板方法模式中子类与抽象类的流程耦合,耦合度相对较高。
八、策略模式的性能优化与高级实践
8.1 性能优化
- 策略对象缓存:使用Spring单例Bean管理策略对象,或通过工厂将策略对象存入Map缓存,避免频繁创建销毁;
- 懒加载策略:对于初始化成本高的策略(如依赖外部资源),可采用懒加载方式,在首次使用时才创建策略对象;
- 避免策略过多:若策略数量极多(如数百个),可结合享元模式复用相似策略,或通过动态代理、注解驱动等方式简化策略创建;
- 并发安全处理:使用线程安全的容器(如ConcurrentHashMap)存储策略,避免多线程环境下的并发问题。
8.2 高级实践
- 注解驱动策略注册:通过自定义注解(如
@StrategyType)标记策略类,在项目启动时扫描注解,自动将策略注册到工厂,无需手动注入Map; - 动态配置策略:将策略类型与策略类的映射关系存储在配置中心(如Nacos、Apollo),支持策略的动态上下线,无需重启项目;
- 策略链模式结合:多个策略需要按顺序执行时,可结合链模式,将策略组成链条,依次执行(如多步骤数据校验、多规则过滤);
- 策略模式与Spring Cloud结合:在微服务架构中,不同策略可部署为独立微服务,通过服务发现动态选择策略服务,实现跨服务的策略扩展。
九、总结
策略模式是Java后端开发中解决多分支逻辑问题的“利器”,其核心思想是“封装变化、依赖抽象”,通过抽象策略、具体策略、上下文(或工厂)三个核心角色,实现算法的灵活封装与扩展。
在实际开发中,使用策略模式时需注意“按需设计”,避免过度设计,同时可结合工厂模式、注解驱动、配置中心等技术进一步优化,提升代码的灵活性和可维护性。掌握策略模式,不仅能解决实际业务中的臃肿代码问题,更能加深对“开闭原则”“依赖倒置原则”等设计原则的理解,提升代码设计能力。