Spring Boot项目实现策略模式、简单工厂与模板方法融合实战
今天咱们讲讲在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的依赖注入和自定义注解功能,进一步探索和实现自动扫描和策略注册机制,打造出更适合自己项目的灵活策略框架。
如果大家对文中的内容有任何疑问或者想法,欢迎在评论区留言讨论。