章
目
录
今天咱们讲讲在Spring Boot项目开发中,深入探讨策略模式、简单工厂模式和模板方法模式是如何巧妙融合,发挥强大作用的。本文将结合一个真实的“数据导出”业务需求,带大家一步步领略这几种设计模式协同工作的魅力。
一、需求背景
假设我们正在开发一个涉及多家银行数据导入导出的应用模块。在实际业务场景中,不同银行的数据处理逻辑往往大相径庭,而且随着业务的发展,新的银行可能会不断加入,原有的银行数据处理细节也可能发生变化。这就给我们的系统设计带来了不小的挑战:
- 要能根据不同的银行编码,准确找到对应的处理策略,实现定制化的数据处理。
- 当有新银行加入时,尽量避免大规模修改已有的代码,遵循开闭原则,保证系统的扩展性。
- 还得提供一套基础的默认逻辑,以防某个银行的处理器未实现相关功能时,程序不会直接崩溃。
二、核心设计思路
为了应对这些挑战,我们采用了策略模式、简单工厂模式和模板方法模式相结合的设计方案。
- 策略模式:简单来说,就是定义一个统一的抽象策略接口(或者抽象类)。把每个银行的数据处理逻辑都当作一个具体的策略来实现,这样就能在运行时根据实际情况灵活切换不同的策略,实现不同银行数据处理逻辑的动态替换。
- 简单工厂模式:创建一个叫
BankStrategyHolder
的策略持有者。它就像一个“策略工厂”,能根据银行编码,从众多策略中动态挑选出对应的策略实例。借助Spring的注解读取元数据功能,它还能自动完成策略的映射,大大简化了策略对象的创建过程。 - 模板方法模式:通过抽象类
BankStrategy
来发挥作用。这个抽象类定义了数据导入导出的基础方法签名,并且提供了默认实现。不过默认实现只是抛出一个不支持的异常,提醒子类去实现具体的逻辑。这样一来,既保证了整体算法框架的统一,又让子类能根据自身需求覆盖特定的方法,实现定制化的处理。
这种设计方式不仅能满足业务灵活扩展的需求,还能让代码结构更加清晰,后期维护起来也更加轻松。
三、关键代码
(一)常量定义
先来看常量定义部分,这部分代码定义了银行编码和默认标识常量,方便在整个项目中统一调用和管理。
public class Constants {
/** 默认策略标识 */
public static final String DEFAULT_STRATEGY_NAME = "default";
}
public class BankConstants {
public static final String FIRST_BANK = "0001";
public static final String SECOND_BANK = "0002";
public static final String THIRD_BANK = "0003";
public static final String FOURTH_BANK = "0004";
}
这里的Constants
类定义了默认策略标识,而BankConstants
类则定义了各个银行对应的编码。在后续的代码中,我们会频繁使用这些常量来区分不同的银行和策略。
(二)自定义注解
为了让工厂能自动扫描和映射不同银行的策略实现,我们使用了自定义注解@StrategyIdentifier
。
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface StrategyIdentifier {
String[] value();
}
这个注解被标记在具体的策略实现类上,value()
方法用来指定该策略所对应的银行编码。通过这种方式,工厂就能轻松识别每个策略实现类对应的银行,实现自动映射。
(三)抽象策略模板
BankStrategy
抽象类在整个设计中扮演着模板方法模式的关键角色。
public abstract class BankStrategy {
/**
* 导入数据,默认不支持。
*/
public void importData() {
throw new UnsupportedOperationException("此银行不支持导入数据功能!");
}
/**
* 导出数据,默认不支持。
*/
public void exportData() {
throw new UnsupportedOperationException("此银行不支持导出数据功能!");
}
}
它定义了数据导入和导出的方法,但默认实现都是抛出不支持的异常。这是因为不同银行的具体导入导出逻辑是不同的,需要子类去覆盖这些方法来实现各自的业务逻辑。
(四)具体策略实现
下面是各个银行具体的策略实现类,它们都继承自BankStrategy
,并结合@StrategyIdentifier
注解来完成不同银行的导入导出逻辑。
@Slf4j
@Component
@StrategyIdentifier({Constants.DEFAULT_STRATEGY_NAME})
public class DefaultBankStrategy extends BankStrategy {
@Override
public void importData() {
log.info("默认银行数据导入逻辑");
}
@Override
public void exportData() {
log.info("默认银行数据导出逻辑");
}
}
@Slf4j
@Component
@StrategyIdentifier({BankConstants.FIRST_BANK})
public class FirstBankStrategy extends BankStrategy{
@Override
public void importData() {
log.info("第一银行数据导入逻辑");
}
@Override
public void exportData() {
log.info("第一银行数据导出逻辑");
}
}
@Slf4j
@Component
@StrategyIdentifier({BankConstants.SECOND_BANK})
public class SecondBankStrategy extends BankStrategy{
@Override
public void importData() {
log.info("第二银行数据导入逻辑");
}
@Override
public void exportData() {
log.info("第二银行数据导出逻辑");
}
}
@Slf4j
@Component
@StrategyIdentifier({BankConstants.THIRD_BANK, BankConstants.FOURTH_BANK})
public class ThirdBankStrategy extends BankStrategy{
// 不重写 importData,默认不支持
@Override
public void exportData() {
log.info("第三、四银行数据导出逻辑");
}
}
每个具体策略实现类都根据自身银行的业务需求,重写了importData
和exportData
方法。@StrategyIdentifier
注解则明确指定了该策略对应的银行编码,方便工厂进行管理。
(五)策略持有者(简单工厂)
BankStrategyHolder
类就像是一个“策略工厂”,负责管理所有的策略实例。
@Component
public class BankStrategyHolder {
private final Map<String, BankStrategy> strategyMap = new HashMap<>();
@Autowired
public BankStrategyHolder(List<BankStrategy> bankStrategyList) {
bankStrategyList.forEach(strategy -> {
StrategyIdentifier annotation = strategy.getClass().getAnnotation(StrategyIdentifier.class);
if (annotation != null) {
for (String bankCode : annotation.value()) {
strategyMap.put(bankCode, strategy);
}
}
});
}
/**
* 根据银行编码获取对应策略,若不存在则返回默认策略。
*/
public BankStrategy getByBankCode(String bankCode) {
return strategyMap.getOrDefault(bankCode, strategyMap.get(Constants.DEFAULT_STRATEGY_NAME));
}
}
在这个类中,通过@Autowired
注解,Spring会自动注入所有的BankStrategy
实现类。然后,它遍历这些策略实例,通过注解读取每个策略对应的银行编码,并将其存储在strategyMap
中。getByBankCode
方法则根据传入的银行编码,从strategyMap
中获取对应的策略实例,如果找不到,则返回默认策略。
(六)外部调用控制器
StrategyController
负责处理外部的请求,通过银行编码获取相应的策略,并调用数据导入导出方法。
@Slf4j
@RestController
@Tag(name = "策略模式示例")
public class StrategyController {
@Resource
private BankStrategyHolder bankStrategyHolder;
@GetMapping("/importData")
@Operation(summary = "导入数据")
public void importData(@RequestParam String bankCode) {
log.info("调用 [导入数据] - 银行编码: {}", bankCode);
bankStrategyHolder.getByBankCode(bankCode).importData();
}
@GetMapping("/exportData")
@Operation(summary = "导出数据")
public void exportData(@RequestParam String bankCode) {
log.info("调用 [导出数据] - 银行编码: {}", bankCode);
bankStrategyHolder.getByBankCode(bankCode).exportData();
}
}
在这个控制器中,通过注入BankStrategyHolder
,获取对应的策略实例,然后根据请求的银行编码调用相应的导入或导出方法。这样,外部请求就能顺利地通过控制器,找到对应的策略并执行相应的业务逻辑。
四、效果验证
我们可以通过向接口/importData
和/exportData
传递不同的银行代码,来验证系统的功能。
- 当向
/importData
接口传入0001
时,会执行第一银行的导入逻辑;传入0002
时,执行第二银行的导入逻辑。 - 传入
0003
或0004
时,由于这两个银行在导入功能上没有具体实现,会抛出异常,提示导入功能不支持。 - 传入其他未定义的银行代码(如
0005
)时,会执行默认的导入逻辑。
同样,在访问/exportData
接口时:
- 传入
0001
、0002
会分别输出各自银行的导出逻辑。 - 传入
0003
、0004
会共用第三银行策略的导出逻辑。 - 未匹配到的银行代码则执行默认导出逻辑。
通过这样的设计,当需要新增银行时,我们只需要新增对应的策略实现类,并添加相应的注解,而无需修改已有的核心代码,充分体现了系统的可扩展性。
五、思考与总结
通过这个实际案例,我们深刻体会到了这几种设计模式协同工作带来的强大优势:
- 策略模式让不同银行的数据导入导出逻辑相互独立,隔离了业务差异,使得替换和扩展算法变得轻而易举。
- 简单工厂模式自动管理策略类的创建和注入过程,让客户端代码变得简洁,降低了代码之间的耦合度。
- 模板方法模式保证了公共方法逻辑的一致性,提供了默认行为,增强了系统的可靠性。
这种设计方式完全符合面向对象的设计原则,极大地提升了代码的可维护性和拓展性。随着业务的不断发展,新增银行业务逻辑时,我们只需要编写新的策略实现类,就能轻松融入现有系统,让系统始终保持简洁高效。
最后,建议大家在学习和实践过程中,充分利用Spring的依赖注入和自定义注解功能,进一步探索和实现自动扫描和策略注册机制,打造出更适合自己项目的灵活策略框架。
如果大家对文中的内容有任何疑问或者想法,欢迎在评论区留言讨论。