今天咱们讲讲在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("第三、四银行数据导出逻辑"); } } 

每个具体策略实现类都根据自身银行的业务需求,重写了importDataexportData方法。@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时,执行第二银行的导入逻辑。
  • 传入00030004时,由于这两个银行在导入功能上没有具体实现,会抛出异常,提示导入功能不支持。
  • 传入其他未定义的银行代码(如0005)时,会执行默认的导入逻辑。

同样,在访问/exportData接口时:

  • 传入00010002会分别输出各自银行的导出逻辑。
  • 传入00030004会共用第三银行策略的导出逻辑。
  • 未匹配到的银行代码则执行默认导出逻辑。

通过这样的设计,当需要新增银行时,我们只需要新增对应的策略实现类,并添加相应的注解,而无需修改已有的核心代码,充分体现了系统的可扩展性。

五、思考与总结

通过这个实际案例,我们深刻体会到了这几种设计模式协同工作带来的强大优势:

  • 策略模式让不同银行的数据导入导出逻辑相互独立,隔离了业务差异,使得替换和扩展算法变得轻而易举。
  • 简单工厂模式自动管理策略类的创建和注入过程,让客户端代码变得简洁,降低了代码之间的耦合度。
  • 模板方法模式保证了公共方法逻辑的一致性,提供了默认行为,增强了系统的可靠性。

这种设计方式完全符合面向对象的设计原则,极大地提升了代码的可维护性和拓展性。随着业务的不断发展,新增银行业务逻辑时,我们只需要编写新的策略实现类,就能轻松融入现有系统,让系统始终保持简洁高效。

最后,建议大家在学习和实践过程中,充分利用Spring的依赖注入和自定义注解功能,进一步探索和实现自动扫描和策略注册机制,打造出更适合自己项目的灵活策略框架。

如果大家对文中的内容有任何疑问或者想法,欢迎在评论区留言讨论。