策略模式 + 工厂方法模式在项目中的实战应用
今天聊一聊策略模式。策略模式是我们在软件开发过程中最常用的设计模式之一。
不过在讲概念之前,我们先聊聊代码的逻辑组织。
在组织代码逻辑时,最常用的表达方式就是 if......else......,以及 for、while 等流程控制语句。其实,当代码中开始出现 if......else if...... 的多分支结构时,我们就应该停下来想一想:这里是否需要引入策略模式?当然,并不是所有的 if......else if...... 都要用策略模式来重构,关键要看每个分支中承载的业务逻辑是什么。
现在回到策略模式的概念。策略模式(Strategy Pattern)的定义是:
定义一系列算法类,将每一个算法封装起来,并让它们可以互相替换。策略模式让算法独立于使用它的客户而变化。它也称为政策模式(Policy),是一种对象行为型模式。
为什么 if......else if...... 和策略模式有天然的联系?因为这种多分支结构往往是在处理同一类事物的不同情况------每个分支对应一种处理方式,而这些不同的处理方式,恰恰就是可以被封装起来的业务行为(算法)。
因此,面对 if......else if...... 的多分支场景,我们可以考虑这样做:
- 抽象算法模型:把各个分支中的具体业务处理逻辑抽象出来,定义成统一的接口或抽象类;
- 封装具体算法:让每个分支的逻辑各自实现为一个独立的算法类;
- 引入工厂管理:配合工厂方法模式,把这些算法类统一管理和创建;
- 按需调用:业务调用时,通过工厂方法返回对应的具体算法,替代原有的分支判断。
下面,我们用一个"数据脱敏"的例子,来展示如何通过"策略模式 + 工厂方法模式"的组合,实现代码的高可扩展性。
一、业务场景描述
在用户信息管理中(可以是患者信息、居民健康档案信息等业务场景),在 C 端查询个人信息时,返回结果包含姓名、身份证号、手机号、邮箱、地址等敏感字段,出于个人隐私保护的需求,需要对上述敏感字段进行脱敏处理,具体规则如下:
- 手机号:保留前 3 位 + 后 4 位(
138****5678) - 身份证:保留前 6 位 + 后 4 位(
110101********8888) - 邮箱:保留用户名前 3 位(
zha***@example.com) - 姓名:保留姓氏(
张**) - 地址:保留行政区划,对道路/门牌/地标做掩码
这时,如果不使用设计模式,很多人的实现方式可能如下------完全使用 if......else if...... 来处理这段逻辑:
typescript
public String sensitive(String value, String type){
if ("name".equals(type)) {
return value.substring(0, 1) +
"*".repeat(value.length() - 1);
} else if ("idcard".equals(type)) {
......
} else if ("email".equals(type)) {
......
} else if ("phone".equals(type)) {
......
} else if ("address".equals(type)) {
......
}
return value;
}
这种写法看上去简单,代码行数不多,结构也清晰。如果是小项目(比如给小公司做的小程序、内部管理系统),需求基本一次定型、变更不多,这样写没有问题。但如果是大规模的企业级软件产品,就可能出问题,比如:
- 可变性需求:患者小程序返回患者信息,在给 A 医院上线时,要求姓名只保留姓,名字全部使用"*"脱敏;而到了 B 医院,要求只对名字中间的字符脱敏,两个字的名字则脱敏最后一个字。也就是说,同一套产品要在不同项目中应对不同的脱敏规则。
- 可扩展性需求:随着项目的积累,需要脱敏的字段类型会越来越多,这个
if......else if......会越来越长,每次新增类型都要修改这个脱敏方法。
下面一步一步拆解:如何使用策略模式和工厂方法模式,让脱敏功能同时具备良好的可变性与可扩展性。
二、抽象算法
2.1 抽象算法策略
所有脱敏操作都有一个共同特征:输入一个字符串,根据脱敏规则,返回脱敏后的字符串。基于这个共同特征,可以抽象出统一的策略接口:
typescript
/**
* 脱敏策略
*
* @version 1.0
* @author: big-bread
* @date: 2025/8/13 10:45
*/
public interface SensitiveStrategy {
String mask(String value);
}
抽象出这个接口后,算法"种类"的扩展问题就解决了:新增一种脱敏类型,只需新增一个接口实现类。但还有一个问题没解决------如果同一种算法在不同项目中有不同的参数要求(比如手机号脱敏,有的项目要求保留前 3 位,有的要求保留前 2 位),又该怎么办?
2.2 算法可变性分析
下面分析脱敏算法中常见的可变因素:
- 脱敏位置:前面保留几位、后面保留几位;
- 替换字符:
*、#等; - 特殊规则:比如需要借助正则表达式做分段处理。
实际项目中的可变因素可能更多,这里先考虑以上三类。既然可变因素可以归纳出来,就可以把它们从代码中抽离,纳入配置管理。本例将配置放在 yml 文件中(实际项目可放到配置中心或数据库,那需要更详细的设计):
yaml
sensitive:
strategies: # 敏感数据处理策略
phone: # 保留前 3 位 + 中间替换 + 保留后 4 位
mask-char: "*"
prefix-keep: 3
suffix-keep: 4
idcard: # 保留前 6 位(地区码)+ 中间替换 + 保留后 4 位
mask-char: "*"
prefix-keep: 6
suffix-keep: 4
email: # 用户名部分保留前 3 位,@ 及域名部分完整保留
mask-char: "*"
prefix-keep: 3
name: # 保留第 1 个字,其余全部替换
mask-char: "*"
prefix-keep: 1
address: # 优先用正则拆分行政区划和详细地址:行政区划完整保留,详细地址中的路名、门牌号、地标建筑分别替换脱敏。示例:北京市朝阳区建国路88号 → 北京市朝阳区****路**号
prefix-keep: 4
custom-pattern: '(.*?[区县旗])(.*)'
default: # 通用兜底策略:保留前 2 位 + 保留后 2 位;长度 < 3 时仅保留首字符
mask-char: "*"
prefix-keep: 2
suffix-keep: 2
说明:
- mask-char :脱敏使用的替换字符,通常为
*; - prefix-keep:头部保留长度;
- suffix-keep:尾部保留长度;
- custom-pattern:自定义正则表达式。
配置抽离之后,策略接口也要相应调整:把可变的配置信息作为参数,传递给具体的策略。
2.3 算法参数抽象
将配置信息抽象为配置类:
typescript
@Configuration
@ConfigurationProperties(prefix = "sensitive")
public class SensitiveConfig {
private Map<String, StrategyConfig> strategies = new HashMap<>();
@Data
public static class StrategyConfig {
private String maskChar;
private Integer prefixKeep;
private Integer suffixKeep;
private String customPattern;
}
/**
* 获取策略配置:未配置时使用 default 兜底策略
*/
public StrategyConfig getStrategyConfig(String strategyKey) {
return strategies.getOrDefault(strategyKey,
strategies.getOrDefault("default", new StrategyConfig()));
}
/**
* 默认策略的 key
*/
public String getDefaultStrategy() {
return "default";
}
public void setStrategies(Map<String, StrategyConfig> strategies) {
this.strategies = strategies;
}
public Map<String, StrategyConfig> getStrategies() {
return this.strategies;
}
}
重新定义策略接口:
arduino
public interface SensitiveStrategy {
String mask(String value, SensitiveConfig.StrategyConfig config);
}
2.4 具体算法实现
具体实现需要考虑配置的缺省情况:配置项缺失时提供默认值(如果项目要求强校验,也可以直接抛出异常)。下面给出几个实现示例,手机号、邮箱等策略的实现方式类似,不再赘述。
2.4.1 姓名脱敏策略
typescript
@Component
public class NameSensitiveStrategy implements SensitiveStrategy {
@Override
public String mask(String value, SensitiveConfig.StrategyConfig config) {
if (value == null || value.isEmpty()) return value;
int prefixKeep = config.getPrefixKeep() != null ? config.getPrefixKeep() : 1;
String maskChar = config.getMaskChar() != null ? config.getMaskChar() : "*";
if (value.length() == 1) return maskChar;
// 防止保留位数超过字符串长度导致越界
int actualPrefixKeep = Math.min(prefixKeep, value.length());
return value.substring(0, actualPrefixKeep)
+ maskChar.repeat(value.length() - actualPrefixKeep);
}
}
2.4.2 身份证脱敏策略
arduino
@Component
public class IdCardSensitiveStrategy implements SensitiveStrategy {
@Override
public String mask(String value, SensitiveConfig.StrategyConfig config) {
if (value == null || value.length() < 15) return value;
int prefixKeep = config.getPrefixKeep() != null ? config.getPrefixKeep() : 6;
int suffixKeep = config.getSuffixKeep() != null ? config.getSuffixKeep() : 4;
String maskChar = config.getMaskChar() != null ? config.getMaskChar() : "*";
// 防止前后保留位数之和超过总长度
int actualPrefixKeep = Math.min(prefixKeep, value.length());
int actualSuffixKeep = Math.min(suffixKeep, value.length() - actualPrefixKeep);
int maskLength = value.length() - actualPrefixKeep - actualSuffixKeep;
return value.substring(0, actualPrefixKeep)
+ maskChar.repeat(maskLength)
+ value.substring(value.length() - actualSuffixKeep);
}
}
2.4.3 地址脱敏策略
ini
/**
* 地址脱敏策略
*
* @version 1.0
* @author: big-bread
* @date: 2025/8/15 11:08
*/
@Component
public class AddressSensitiveStrategy implements SensitiveStrategy {
@Override
public String mask(String value, SensitiveConfig.StrategyConfig config) {
if (value == null || value.isEmpty()) return value;
int prefixKeep = config.getPrefixKeep() != null ? config.getPrefixKeep() : 4;
String maskChar = config.getMaskChar() != null ? config.getMaskChar() : "*";
// 如果有自定义正则,优先使用
if (config.getCustomPattern() != null) {
Pattern pattern = Pattern.compile(config.getCustomPattern());
Matcher matcher = pattern.matcher(value);
if (matcher.find()) {
// 行政区划部分(包含区字)
String administrativeArea = matcher.group(1);
// 详细地址部分(可能为空)
String detail = matcher.groupCount() > 1 ? matcher.group(2) : "";
// 只有在存在详细地址时才进行脱敏
if (detail != null && !detail.isEmpty()) {
detail = processDetail(detail);
} else if (detail == null) {
detail = "";
}
return administrativeArea + detail;
}
return processDetail(value);
}
// 确保 prefixKeep 不超过字符串长度
int actualPrefixKeep = Math.min(prefixKeep, value.length());
int maskLength = Math.max(0, value.length() - actualPrefixKeep);
// 保留前缀部分
String prefix = value.substring(0, actualPrefixKeep);
// 生成掩码部分
String mask = maskChar.repeat(maskLength);
return prefix + mask;
}
private String processDetail(String detail) {
// 处理道路名称
detail = detail.replaceAll(
"[\u4e00-\u9fa5]+?(路|街|道|巷|弄|段|里|坊|大街|大道|公路|高速)",
"****$1"
);
// 处理门牌号
detail = detail.replaceAll(
"\d+(号|栋|幢|室|单元|楼|层|座|号楼|栋楼|幢楼)",
"**$1"
);
// 处理地标建筑
detail = detail.replaceAll(
"[\u4e00-\u9fa5]{2,}(社区|广场|大厦|中心|花园|小区|公寓|酒店|商场|写字楼|科技园|工业园)",
"****$1"
);
return detail;
}
}
2.4.4 默认兜底策略
yml 中定义了 default 兜底配置,还需要一个对应的实现类,工厂才能按 default 这个 key 找到它:
arduino
/**
* 默认兜底脱敏策略
*
* @version 1.0
* @author: big-bread
* @date: 2025/8/15 11:20
*/
@Component
public class DefaultSensitiveStrategy implements SensitiveStrategy {
@Override
public String mask(String value, SensitiveConfig.StrategyConfig config) {
if (value == null || value.isEmpty()) return value;
int prefixKeep = config.getPrefixKeep() != null ? config.getPrefixKeep() : 2;
int suffixKeep = config.getSuffixKeep() != null ? config.getSuffixKeep() : 2;
String maskChar = config.getMaskChar() != null ? config.getMaskChar() : "*";
// 长度过短时仅保留首字符
if (value.length() < 3) {
return value.substring(0, 1) + maskChar.repeat(value.length() - 1);
}
int actualPrefixKeep = Math.min(prefixKeep, value.length());
int actualSuffixKeep = Math.min(suffixKeep, value.length() - actualPrefixKeep);
int maskLength = value.length() - actualPrefixKeep - actualSuffixKeep;
return value.substring(0, actualPrefixKeep)
+ maskChar.repeat(maskLength)
+ value.substring(value.length() - actualSuffixKeep);
}
}
2.5 小节总结
以上实现只是简单示例(AI 时代,具体算法可以交给 AI 生成,但设计思想仍需要由我们传递给 AI)。本节的核心是做了两层抽象:
- 算法抽象:将脱敏行为抽象为统一接口,解决算法种类的扩展性问题;
- 参数抽象:将算法中的可变因素抽象为配置,解决同一算法在不同场景下的可变性问题。
通过算法抽象获得扩展性,通过参数抽象获得可变性。
三、策略管理
策略封装完成后,接下来的问题是:策略怎么使用?如果还是在业务代码里写 if......else if...... 来调用策略,那等于没有摆脱分支结构。下面从使用场景出发,分析脱敏能力应该怎么接入:
- 脱敏的时机:脱敏通常针对返回给前端的 VO 对象。可以开发自定义注解标注需要脱敏的字段,再通过 AOP 切面在接口返回结果时统一处理。
- 策略的调用:用工厂方法把策略统一注册到容器中;注解触发脱敏时,按策略 key 从工厂中获取对应策略执行,用脱敏后的数据替换原数据。
3.1 工厂方法封装脱敏策略
定义工厂类:
typescript
/**
* 脱敏策略工厂类
*
* @version 1.0
* @author: big-bread
* @date: 2025/8/13 18:40
*/
@Component
public class SensitiveContext {
private final Map<String, SensitiveStrategy> strategyMap = new ConcurrentHashMap<>();
/**
* 构造函数,自动注册所有策略实现
* @param strategies 策略实现列表
*/
@Autowired
public SensitiveContext(List<SensitiveStrategy> strategies) {
// 自动注册所有策略实现
for (SensitiveStrategy strategy : strategies) {
String key = strategy.getClass().getSimpleName()
.replace("SensitiveStrategy", "")
.toLowerCase();
registerStrategy(key, strategy);
}
}
/**
* 注册策略
* @param key 策略的 key
* @param strategy 策略实现
*/
public void registerStrategy(String key, SensitiveStrategy strategy) {
strategyMap.put(key, strategy);
}
/**
* 执行指定的策略
* @param key 策略的 key
* @param value 待处理的数据
* @param config 策略的配置
* @return 脱敏后的数据
*/
public String executeStrategy(String key, String value, SensitiveConfig.StrategyConfig config) {
SensitiveStrategy strategy = strategyMap.get(key);
if (strategy == null) {
throw new IllegalArgumentException("No strategy registered for key: " + key);
}
return strategy.mask(value, config);
}
/**
* 获取所有已注册的策略
* @return 已注册的策略 key 集合
*/
public Set<String> getAvailableStrategies() {
return strategyMap.keySet();
}
}
说明:
- 自动注册机制 :新增策略只需编写实现类并标注
@Component,无需修改任何注册代码; - 策略管理 :使用
ConcurrentHashMap管理策略,保证多线程环境下的读写安全(本例属于防御性设计,兼顾未来动态更新配置的可能); - 策略与配置分离 :策略本身不持有配置,由外部传入
StrategyConfig。同一个策略实现可以搭配不同配置参数复用(如 yml 中定义的prefix-keep、suffix-keep等),实现算法与参数的解耦。
3.2 脱敏注解封装
定义处理标记注解(类/方法级):
标注在 Controller 的类或方法上,用于精确圈定切面的拦截范围。
less
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveProcess {
}
定义字段注解:
标注在 VO 的字段上,声明该字段使用哪种策略脱敏。
less
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Sensitive {
/**
* 策略键值,对应注册的策略名称
*/
String strategyKey() default "";
/**
* 是否启用,可动态控制
*/
boolean enabled() default true;
}
定义注解切面:
kotlin
/**
* 脱敏注解切面
*
* @version 1.0
* @author: big-bread
* @date: 2025/8/13 18:48
*/
@Aspect
@Component
public class SensitiveAspect {
@Autowired
private SensitiveContext sensitiveContext;
@Autowired
private SensitiveConfig sensitiveConfig;
// 缓存字段注解信息,避免重复反射
private final Map<Class<?>, Map<Field, Sensitive>> annotationCache = new ConcurrentHashMap<>();
@Around("@annotation(com.big.bread.export.common.sensitive.SensitiveProcess) || @within(com.big.bread.export.common.sensitive.SensitiveProcess)")
public Object processControllerResponse(ProceedingJoinPoint joinPoint) throws Throwable {
Object result = joinPoint.proceed();
if (result instanceof ResponseResult) {
Object data = processSensitiveData(((ResponseResult<?>) result).getData());
return ResponseUtil.makeSuccess(data);
}
return processSensitiveData(result);
}
private Object processSensitiveData(Object data) {
if (data == null) return null;
// 处理集合
if (data instanceof Collection) {
return ((Collection<?>) data).stream()
.map(this::processSingleObject)
.collect(Collectors.toList());
}
// 处理数组
if (data.getClass().isArray()) {
return Arrays.stream((Object[]) data)
.map(this::processSingleObject)
.toArray();
}
// 处理单对象
return processSingleObject(data);
}
private Object processSingleObject(Object obj) {
if (obj == null) return null;
Class<?> clazz = obj.getClass();
Map<Field, Sensitive> desensitizationFields = getSensitiveFields(clazz);
desensitizationFields.forEach((field, annotation) -> {
if (!annotation.enabled()) return;
try {
field.setAccessible(true);
Object value = field.get(obj);
if (value instanceof String) {
// 获取策略配置
String strategyKey = getEffectiveStrategyKey(annotation);
SensitiveConfig.StrategyConfig config = sensitiveConfig.getStrategyConfig(strategyKey);
// 执行脱敏
String maskedValue = sensitiveContext.executeStrategy(
strategyKey,
(String) value,
config
);
field.set(obj, maskedValue);
}
} catch (IllegalAccessException e) {
// 记录日志,避免静默失败
}
});
return obj;
}
private String getEffectiveStrategyKey(Sensitive annotation) {
if (!annotation.strategyKey().isEmpty()) {
return annotation.strategyKey();
}
return sensitiveConfig.getDefaultStrategy();
}
private Map<Field, Sensitive> getSensitiveFields(Class<?> clazz) {
return annotationCache.computeIfAbsent(clazz, k -> {
Map<Field, Sensitive> fields = new HashMap<>();
for (Field field : clazz.getDeclaredFields()) {
Sensitive desensitization = field.getAnnotation(Sensitive.class);
if (desensitization != null) {
fields.put(field, desensitization);
}
}
return fields;
});
}
}
四、使用示例
下面是一个居民基本信息 VO 对象的脱敏注解使用示例:
typescript
@Data
public class ResidentInfoVO {
// 人员ID(主键)
private String residentId;
// 健康档案编号
private String healthRecordNo;
// 姓名
@Sensitive(strategyKey = "name")
private String name;
// 证件类型
private String idType;
// 证件号码
@Sensitive(strategyKey = "idcard")
private String idNumber;
// 出生日期
private LocalDate birthDate;
// 详细地址
@Sensitive(strategyKey = "address")
private String address;
// 联系电话
@Sensitive(strategyKey = "phone")
private String phone;
......
}
然后在 Controller 的类或查询方法上标注 @SensitiveProcess,切面就会在接口返回结果时自动完成脱敏:
less
@RestController
public class ResidentController {
@SensitiveProcess
@GetMapping("/resident/{id}")
public ResponseResult<ResidentInfoVO> detail(@PathVariable String id) {
......
}
}
五、总结
5.1 方案回顾
- 问题起点 :
if......else if......的多分支写法简单直接,但在企业级产品中会遇到两个痛点------可变性 (同一算法在不同项目中的规则不同)和可扩展性(每新增一种类型都要修改原有代码)。 - 两层抽象 :通过算法抽象 (
SensitiveStrategy接口)解决扩展性;通过参数抽象 (StrategyConfig配置类 + yml 配置)解决可变性。 - 策略管理 :通过工厂方法(
SensitiveContext)自动注册和分发策略,调用方不再感知分支判断;再通过"注解 + AOP"(@SensitiveProcess/@Sensitive+SensitiveAspect)把脱敏能力无侵入地接入业务代码------VO 字段加上注解即可完成脱敏。
这套组合带来的收益:
- 符合开闭原则:新增脱敏类型 = 新增一个实现类 + 一行配置,不改动任何既有代码;
- 职责单一:每个策略类只关心自己的算法,可独立编写单元测试;
- 规则配置化:同一套代码通过配置适配不同项目的规则差异,无需为每个项目拉分支改代码;
- 业务无侵入:脱敏逻辑集中在切面中,业务代码零感知。
最后,几点使用时的注意事项:
- 不是所有
if......else都需要策略化:分支少、逻辑稳定、几乎不变的场景,直白的写法反而更好,引入模式本身也是成本; - 明确失败策略:脱敏涉及安全合规,脱敏失败时是"放行原值"还是"拒绝返回",需要按合规要求显式定义(本文示例为简化做了放行处理,生产环境建议失败时记录日志并按需熔断);
- 策略膨胀的管理:策略和配置项变多之后,需要配套的命名规范与文档,并对配置做校验和默认值兜底;
- 性能意识 :反射操作务必做缓存(本文示例已使用
annotationCache),避免影响接口性能。
设计模式的价值不在于背诵概念,而在于识别"代码中哪里会变",并把变化隔离出来。AI 时代,具体的算法实现可以交给 AI 生成,但"哪里会变、如何隔离变化"的判断,仍然来自工程师自己。
5.2 策略模式场景扩展
其实,我们在基于能力封装的时候,大多都可以使用到了策略模式,下面我列举一些场景供参考:
- 文件导出:POI、EasyExcel、iText、Freemarker → 按 excel / pdf / word 选择策略
- 消息队列:RocketMQ、RabbitMQ、Kafka → 按 MQ 类型选择生产者策略
- 事件总线:Spring Event、Guava EventBus → 本质就是按事件类型路由,与策略注册表同构
- 文件存储:本地、MinIO、OSS、S3 → local / minio / oss / s3
- 通知触达:短信、邮件、App 推送、IM → sms / mail / push
- 支付渠道:微信、支付宝、银联 → wxpay / alipay / unionpay
- 另外还有登录认证、加解密算法、检索引擎、缓存体系
为什么上面的场景需要考虑策略?常见原因主要两方面:
- 客户原因:客户现有环境利旧、受政策要求必须使用某些技术栈或中间件。
- 能力提供方变化:能力提供方开源协议变化,如闭源等,有可能导致技术栈变更。
以上原因就需要针对能力的封装考虑策略模式,针对不同的能力提供方,抽象能力,业务端调用只使用能力而不关注能力的具体实现。