ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

策略模式 + 反射工厂:优雅实现开闭原则的深入实践

策略模式 + 反射工厂:优雅实现开闭原则的深入实践 一、开闭原则的本质在探讨策略模式和反射工厂之前我们有必要先把“开闭原则”这个概念理解透彻。开闭原则是面向对象设计原则中非常重要的一条它的英文全称是 Open-Closed Principle简称 OCP。开闭原则的核心表述是软件实体应当对扩展开放对修改关闭。这里的软件实体可以是一个类、一个模块、一个函数也可以是一个完整的系统。很多初学者会把开闭原则简单理解为“加功能不要改代码”这个理解方向是对的但还不够准确。更准确的含义是当系统需要增加新的能力、新的业务规则、新的处理分支时我们应该通过增加新的代码单元来实现而不是去修改已经稳定运行的核心代码。已经存在的类、方法、接口应当尽量保持不动这样原本经过测试、线上验证的代码就不会因为频繁改动而引入新的风险。为什么开闭原则这么重要在真实的业务系统中需求变化几乎是不可能避免的。一个电商系统今天要支持满减优惠明天要支持会员折扣后天又要增加节日促销活动。如果我们每次增加一种优惠方式都要回到原来的计算逻辑里修改 if-else 分支那么随着分支越来越多代码会变得非常臃肿一个方法可能膨胀到几百行里面充斥着各种条件嵌套、魔数和重复判断。更严重的是任何一次修改都可能影响已经上线的业务逻辑因为所有分支都挤在一起牵一发而动全身。开闭原则的价值就在于它提供了一种结构化的应对变化的方式。它要求我们把系统中容易变化的部分抽象出来形成稳定的扩展点而变化本身则被隔离在独立的实现单元中。这样当出现新的需求时我们只需要新增一个实现单元而不需要触碰原有的核心逻辑。原有的核心逻辑不需要重新测试新的功能可以独立测试风险被控制在一个更小的范围内。要实现开闭原则仅仅依靠一条设计原则是不够的还需要借助具体的设计模式来落地。其中策略模式负责抽象变化行为工厂模式负责管理对象创建而反射工厂则进一步降低了工厂本身对具体实现类的依赖。这三者组合起来能够形成一套非常优雅的可扩展架构。下面的内容会逐步展开这套方案的设计思路、代码实现、性能优化和实践注意事项。二、策略模式为什么能支撑开闭原则策略模式是一种行为型设计模式它定义了一系列算法并将每个算法封装起来使它们之间可以互相替换。策略模式让算法的变化独立于使用算法的客户端。简单来说策略模式就是把一组可以互相替换的处理逻辑从使用逻辑的主体代码中抽离出来封装成一个个独立的对象。策略模式通常包含三个角色策略接口、具体策略实现和上下文对象。策略接口定义所有策略需要遵守的统一行为比如一个计算方法具体策略实现分别实现这个接口完成不同的算法上下文对象持有策略接口的引用并在运行时根据业务需要选择某个具体策略来执行。策略模式天然契合开闭原则。因为一旦我们把策略抽象成接口客户端依赖的就是接口而不是具体实现。当新增一种策略时我们只需要新增一个实现类并让它在合适的时候被选择原有的接口、上下文、已经存在的策略实现类都不需要改动。这就实现了“对扩展开放对修改关闭”。不过策略模式本身并没有解决“如何选择策略”的问题。它解决的是“如何把多变的算法从主体逻辑中剥离出来”而选择策略的动作通常仍然需要一段判断逻辑。很多团队在实践策略模式时会把策略实现类写好了但在调用处仍然保留一大堆 if-else 或者 switch-case 来根据类型编码决定使用哪个策略。这样做虽然比把所有算法都写在判断里要好一些但判断代码仍然会随着策略数量的增加而增加本质上并没有完全关闭修改。真正能够更彻底地解决问题的方式是把“策略选择”也抽象出来形成工厂。工厂负责根据业务参数返回对应的策略对象客户端只需要向工厂索取策略而不需要知道如何创建、如何选择。反射工厂就是其中一种非常灵活的实现方式。三、反射工厂为什么是策略模式的天然搭档工厂模式的核心职责是创建对象而策略模式的核心职责是封装可替换的算法。当我们把策略类管理起来时工厂就成为了策略对象的统一入口。最简单的工厂可能是这样的传入一个策略类型工厂内部用 switch-case 判断然后手动 new 一个对应的策略对象返回。但这种写法仍然存在与策略选择逻辑耦合的问题。使用反射实现的工厂可以显著减少工厂内部对具体策略类的硬编码依赖。所谓反射就是 Java 等语言提供的在运行时动态获取类信息、创建对象、调用方法的能力。反射工厂不需要在代码里显式地 new 某个策略类而是根据配置或参数中的类名在运行时通过 Class.forName 加载对应的类再通过 newInstance 等方式创建实例。反射工厂的好处在于当新增一个策略实现类时我们不一定要修改工厂代码。如果工厂设计为根据映射关系从配置文件中读取类名那么新增策略时只需要做两件事第一编写新的策略实现类第二在配置文件中添加一条映射关系。原有的工厂代码、上下文代码、接口代码都不需要改动。这正好符合开闭原则的期望。当然反射工厂也不是银弹。反射调用相比直接 new 会有一定的性能开销代码可读性和编译期类型检查也会有所减弱。因此在实际项目中我们通常会结合缓存、配置加载、静态初始化等方式来弥补这些不足。后面会详细说明这些优化手段。策略模式和反射工厂组合起来形成了一种非常常见的架构思路策略接口定义稳定扩展点具体策略类承载变化反射工厂负责按照配置动态创建并返回正确的策略实例客户端只依赖接口和工厂。这样变化的三个关键环节——策略实现、策略选择和策略创建——都被组织得比较清晰。四、组合设计策略模式 反射工厂的整体架构我们可以先站在更高的层面看一下这套组合架构的整体模样。假设我们正在开发一个订单结算系统结算时需要根据不同的营销活动计算优惠金额。业务上存在多种优惠策略普通无优惠、会员折扣、满减、优惠券、节日折扣、新人首单优惠等。不同的策略处理逻辑不同而且未来还可能不断增加新的策略。按照策略模式加反射工厂的设计我们至少会有以下几个部分策略接口定义统一的优惠计算行为比如 calculate 方法。具体策略实现每一种优惠方式对应一个实现类每个类只关注自己的计算规则。策略工厂使用反射机制根据策略编码或类型从配置或注册表中找到对应的实现类名并创建策略实例。策略配置维护策略编码和实现类名之间的映射关系可以是 properties 文件、yaml 文件、数据库表或枚举配置。客户端调用业务代码只需要拿到策略编码向工厂请求策略对象然后调用统一的计算方法。这种架构下核心业务代码中几乎不会出现针对具体策略类型的条件判断新增策略时也不需要修改核心业务代码。策略的扩展被隔离在策略实现类和配置文件两个位置这让系统对需求变化的适应能力大幅度提升。抽象地看这套方案非常符合“依赖倒置原则”和“里氏替换原则”。客户端依赖的是策略接口和工厂接口而不是具体策略类所有具体策略类都可以替换策略接口的位置行为表现符合接口契约。反射工厂只负责创造对象不参与具体业务规则因此保持了职责单一。接下来我们通过一个完整的电商订单折扣案例把每个环节的具体代码和思路讲清楚。五、典型业务案例电商订单折扣策略系统我们以一个常见的电商结算场景为例。假设订单系统中用户可以享受不同类型的优惠常见的包括普通用户不打折、VIP 用户享受固定折扣、满减活动、优惠券抵扣、节日促销折扣等。一开始很多开发者可能会写出下面这样的计算代码public class OrderService { public BigDecimal calculatePayAmount(Order order) { BigDecimal amount order.getOriginalAmount(); String type order.getDiscountType(); if (VIP.equals(type)) { amount amount.multiply(new BigDecimal(0.85)); } else if (FULL_REDUCTION.equals(type)) { if (amount.compareTo(new BigDecimal(300)) 0) { amount amount.subtract(new BigDecimal(50)); } } else if (FESTIVAL.equals(type)) { amount amount.multiply(new BigDecimal(0.9)); } else if (NEW_USER.equals(type)) { amount amount.subtract(new BigDecimal(20)); } return amount; } }这样的代码在业务量较小时似乎能解决问题但随着优惠类型越来越多if-else 分支会不断膨胀。每一次营销活动调整都意味着修改这个方法。即使这种修改只是新增一个分支也仍然打破了开闭原则因为稳定运行的核心结算方法被反复修改了。更麻烦的是如果满减规则改变开发人员需要在庞大的条件分支中定位并修改稍有不慎就可能影响其他分支。这种写法的缺点非常明显第一方法职责过重既负责支付金额计算又内嵌了所有优惠规则第二可读性差业务规则和流程控制混在一起第三可测试性差要测试某一种优惠必须构造出能命中该分支的完整条件第四可维护性差每次新增或修改优惠规则都要动这个核心方法回归风险高。策略模式的目标就是把每一种优惠算法抽离出去而不改变调用方。我们定义一个 DiscountStrategy 接口所有优惠策略都实现它。接着用反射工厂来根据优惠类型创建对应的策略实现消除 if-else 链。六、核心接口与领域模型设计我们先来设计领域模型和策略接口。订单对象需要携带参与优惠计算的信息比如原始金额、会员等级、优惠类型、优惠券编码等。这些信息会作为策略计算的输入。import java.math.BigDecimal; public class Order { private BigDecimal originalAmount; private String memberLevel; private String discountType; private String couponCode; public Order(BigDecimal originalAmount, String memberLevel, String discountType) { this.originalAmount originalAmount; this.memberLevel memberLevel; this.discountType discountType; } public BigDecimal getOriginalAmount() { return originalAmount; } public String getMemberLevel() { return memberLevel; } public String getDiscountType() { return discountType; } public String getCouponCode() { return couponCode; } public void setCouponCode(String couponCode) { this.couponCode couponCode; } }订单类中originalAmount 表示订单原始金额memberLevel 表示会员等级discountType 表示本次应该使用的优惠策略类型couponCode 表示优惠券编码。后面两个字段可以根据具体策略需要选择性使用。策略接口定义统一的优惠计算行为。所有具体策略都实现 calculate 方法并返回计算后的应付金额。我们还可以额外定义 getStrategyCode 方法方便策略自描述自己的编码。import java.math.BigDecimal; public interface DiscountStrategy { BigDecimal calculate(Order order); String getStrategyCode(); }这个接口非常简单但它是整个方案的核心抽象。客户端只依赖 DiscountStrategy 接口因此无论后面增加多少种优惠策略客户端的代码都不需要修改。策略接口的方法设计要尽量稳定因为它相当于所有策略实现的公共契约。我们需要特别注意策略接口中的方法参数和返回值应该能够覆盖所有策略的需要。如果某个策略还需要除订单之外的额外上下文信息可以考虑把上下文封装成一个更大的请求对象而不是频繁修改接口方法。保持接口稳定是实践开闭原则的重要前提。七、具体策略实现接下来编写一系列具体策略类。每个类只关注一种优惠规则这样单个类的职责非常清晰。为了便于使用反射工厂创建实例所有策略类都应该提供公开的无参构造方法。普通无优惠策略import java.math.BigDecimal; public class NormalDiscountStrategy implements DiscountStrategy { Override public BigDecimal calculate(Order order) { return order.getOriginalAmount(); } Override public String getStrategyCode() { return NORMAL; } }VIP 会员折扣策略会员享受 85 折import java.math.BigDecimal; public class VipDiscountStrategy implements DiscountStrategy { Override public BigDecimal calculate(Order order) { BigDecimal amount order.getOriginalAmount(); return amount.multiply(new BigDecimal(0.85)); } Override public String getStrategyCode() { return VIP; } }满减策略订单金额满 300 元减 50 元import java.math.BigDecimal; public class FullReductionDiscountStrategy implements DiscountStrategy { private static final BigDecimal THRESHOLD new BigDecimal(300); private static final BigDecimal REDUCTION new BigDecimal(50); Override public BigDecimal calculate(Order order) { BigDecimal amount order.getOriginalAmount(); if (amount.compareTo(THRESHOLD) 0) { return amount.subtract(REDUCTION); } return amount; } Override public String getStrategyCode() { return FULL_REDUCTION; } }节日促销策略享受 9 折优惠import java.math.BigDecimal; public class FestivalDiscountStrategy implements DiscountStrategy { Override public BigDecimal calculate(Order order) { return order.getOriginalAmount().multiply(new BigDecimal(0.9)); } Override public String getStrategyCode() { return FESTIVAL; } }新人优惠策略在原始金额基础上直接减 20 元import java.math.BigDecimal; public class NewUserDiscountStrategy implements DiscountStrategy { Override public BigDecimal calculate(Order order) { return order.getOriginalAmount().subtract(new BigDecimal(20)); } Override public String getStrategyCode() { return NEW_USER; } }从这些实现类可以看出每一个策略都只包含自己关心的规则。新增一种优惠比如满 500 减 100只需要再写一个实现类并在配置中注册即可完全不用修改已经存在的策略类也不会影响普通策略、会员策略等其他实现。这种“每个变化单独一个类”的粒度使得单个类的复杂度很低测试也非常容易。开发人员可以单独为满减策略编写单元测试而不用担心影响其他策略。这也正是策略模式带来的直接收益。八、反射工厂的完整实现策略实现类虽然已经独立出来但如果调用方仍然需要根据优惠类型手动 new 这些类就还是会出现大量的条件判断。反射工厂的作用就是集中管理策略对象的创建过程把选择逻辑从业务代码中彻底拿掉。一个基本的反射工厂实现如下import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class DiscountStrategyFactory { private static final MapString, DiscountStrategy STRATEGY_CACHE new ConcurrentHashMap(); private DiscountStrategyFactory() { } public static DiscountStrategy getStrategy(String strategyCode) { if (strategyCode null) { throw new IllegalArgumentException(策略编码不能为空); } DiscountStrategy cachedStrategy STRATEGY_CACHE.get(strategyCode); if (cachedStrategy ! null) { return cachedStrategy; } String className StrategyConfig.getClassName(strategyCode); if (className null) { throw new IllegalArgumentException(未找到策略编码对应的实现 strategyCode); } try { Class? strategyClass Class.forName(className); DiscountStrategy strategy (DiscountStrategy) strategyClass.getDeclaredConstructor().newInstance(); STRATEGY_CACHE.put(strategyCode, strategy); return strategy; } catch (ClassNotFoundException e) { throw new IllegalStateException(策略类不存在 className, e); } catch (Exception e) { throw new IllegalStateException(创建策略实例失败 className, e); } } }这个工厂类包含几个关键点。首先它使用一个静态的 ConcurrentHashMap 作为缓存用来保存已经创建过的策略对象。策略对象本身通常是无状态的因此可以安全地被多个线程复用缓存可以避免重复创建带来的开销。其次工厂通过 StrategyConfig 根据策略编码查找到对应的实现类全限定名再使用 Class.forName 加载类然后通过反射创建实例。创建成功后放入缓存并返回。如果配置中不存在对应类名或者类加载、实例化失败工厂会抛出带有明确信息的异常便于排查。反射工厂本身不依赖任何具体策略类它只依赖 DiscountStrategy 接口以及从配置中读到的字符串类名。因此当业务新增策略时这个工厂类不需要做任何修改。这就是反射工厂相比普通工厂最有价值的地方。九、策略配置与加载反射工厂依赖一个映射关系策略编码到实现类全限定名。这个映射关系最好不要硬编码在代码中而是放到配置文件里这样新增策略时只需要修改配置不需要改任何 Java 代码。我们可以准备一个 strategy-config.properties 文件内容如下NORMALcom.example.strategy.NormalDiscountStrategy VIPcom.example.strategy.VipDiscountStrategy FULL_REDUCTIONcom.example.strategy.FullReductionDiscountStrategy FESTIVALcom.example.strategy.FestivalDiscountStrategy NEW_USERcom.example.strategy.NewUserDiscountStrategy对应的配置加载类import java.io.IOException; import java.io.InputStream; import java.util.Properties; public class StrategyConfig { private static final Properties PROPERTIES new Properties(); static { try (InputStream input StrategyConfig.class.getClassLoader().getResourceAsStream(strategy-config.properties)) { if (input ! null) { PROPERTIES.load(input); } } catch (IOException e) { throw new ExceptionInInitializerError(加载策略配置失败); } } private StrategyConfig() { } public static String getClassName(String strategyCode) { return PROPERTIES.getProperty(strategyCode); } }这里使用了 Properties 来读取 classpath 下的配置文件。静态代码块在类加载时执行一次将配置内容加载到内存。getClassName 方法根据策略编码返回对应的实现类全限定名。如果配置中没有该编码则返回 null工厂会抛出明确的异常。使用配置文件的好处是显而易见的。当我们需要新增一个策略时流程变成编写一个实现类在 properties 文件中添加一行映射然后打包部署。已经存在的工厂、配置加载类、业务调用代码都不需要修改。这最大程度地符合了开闭原则。实际项目中配置也可以来自数据库、配置中心或 Nacos、Apollo 等动态配置系统。那样我们还可以在运行时动态刷新策略映射甚至动态上线新策略而不需要重启应用。不过动态刷新会引入更复杂的缓存失效和并发问题需要根据实际情况权衡。十、客户端如何优雅地使用策略有了策略接口、策略实现、反射工厂和配置加载器之后业务调用代码就变得非常简洁了。以一个订单结算服务为例import java.math.BigDecimal; public class OrderService { public BigDecimal calculatePayAmount(Order order) { String discountType order.getDiscountType(); DiscountStrategy strategy DiscountStrategyFactory.getStrategy(discountType); return strategy.calculate(order); } }可以看到原本一大串 if-else 分支被替换成两行代码。先根据订单的优惠类型从工厂拿到策略对象然后直接调用策略的 calculate 方法完成计算。OrderService 中不再包含任何具体的优惠规则也不知道策略对象到底是哪个实现类它只知道 DiscountStrategy 接口。如果未来新增一种优惠策略比如“满 1000 减 200”我们只需要新增一个策略类并在配置文件中添加一条记录。OrderService 完全不用改订单类不用改工厂不用改已经存在的策略也不用改。新增的变化被隔离在新增类和新配置行上这非常符合开闭原则的预期。客户端代码的简洁性也带来了更好的可测试性。我们可以很容易地对 OrderService 进行单元测试只需要 mock 掉工厂返回的策略对象即可。实测中我们更关心的是策略本身的算法是否正确、配置是否完整而不是结算流程有没有正确拼接条件分支。十一、验证开闭原则三种扩展场景演示为了更直观地体会这套方案如何实现开闭原则我们模拟三种业务扩展场景看看每一步需要做的改动范围。第一种场景新增“满 500 减 100”的促销策略。我们需要新建一个 FullReduction500Strategy 类实现 DiscountStrategy 接口在 calculate 方法中实现满 500 减 100 的规则getStrategyCode 返回 FULL_REDUCTION_500。然后在 strategy-config.properties 中添加一行配置。原来的工厂、客户端、其他策略都不需要修改。第二种场景调整 VIP 折扣从 85 折变成 8 折。因为 VIP 策略被封装在 VipDiscountStrategy 类中我们只需要修改这个类里的折扣系数其他策略和调用方完全不受影响。这种修改虽然修改了已有代码但它是被限制在一个具体策略内部没有影响系统的扩展点和其他策略。第三种场景把普通用户也纳入某个新人促销规则。如果原有 NEW_USER 规则只对新人适用现在普通用户也要享受我们可以在订单传入优惠类型时直接带上 NEW_USER而不需要修改任何策略代码。工厂仍然根据编码返回同一个 NewUserDiscountStrategy策略本身不关心调用方是谁。从这三个场景可以看出方案把需求变化控制在了很小的范围内。对于新增需求基本上只需要新增代码和配置对于已有规则的内部调整改动也被限制在对应策略类内部。系统的核心调用链路始终保持稳定。十二、从 if-else 到策略模式的重构过程如果你手头正好有一个充满 if-else 的老系统不一定需要一次性把整个系统全部重写。我们可以采用渐进式重构的方式逐步把庞大条件分支迁移到策略模式加反射工厂的架构上。第一步是识别变化点。找到那些根据某个类型或编码走不同处理分支的逻辑比如订单优惠、支付渠道、文件解析器、消息处理器等。先把这些分支抽成一个接口方法然后用多个实现类分别承载原来的处理逻辑。第二步是引入策略接口。定义统一的处理方法把原来的每个 if 分支中的代码拷贝到对应的策略实现类中。这一步只是搬运代码不改变业务逻辑因此风险可控。搬运完成后客户端原来调用分支逻辑的地方可以改为根据类型拿到策略对象并调用统一方法。第三步是引入工厂。最初可以写一个简单的 Map 注册工厂帮助创建策略对象后续再逐步升级为反射工厂和配置驱动。这样做的好处是每一步都可以独立测试和上线不用一次性承担太大风险。重构到后期你会发现在主流程里几乎看不到具体业务分支主线代码变得很短、很稳定。开发人员新增需求时也能自然地把逻辑塞到新策略类里而不会再去堆 if-else。十三、工厂缓存与单例策略在反射工厂中我们默认对策略对象进行了缓存。这是因为大多数策略实现都是无状态的也就是它们不保存业务请求相关的可变状态。一个无状态对象可以安全地被多个线程共享因此只需要创建一次后续反复使用即可。缓存可以带来两个明显好处。第一是性能提升反射创建对象的成本比直接复用已有实例要高缓存可以避免每次请求都重复进行类加载和实例化。第二是减少对象数量在高并发场景下如果每次请求都创建一个新策略对象会浪费大量内存并增加 GC 压力。但缓存并非总是安全的。如果某个策略类中包含可变字段或者策略的执行过程会修改对象状态那么单例共享就会引发线程安全问题。因此使用缓存的前提是策略对象确实是无状态的。如果某些策略必须携带状态建议不要缓存或者为每次调用创建新实例并由调用方自行管理其生命周期。我们还可以考虑使用枚举来维护策略编码和类名的映射这样编码具有类型安全性和 IDE 提示。但枚举是编译期固定的扩展时需要重新编译。相比之下配置文件更灵活。项目可以根据团队习惯选择合适的方式。十四、线程安全与并发场景并发是生产环境必须面对的问题。在反射工厂中我们使用了 ConcurrentHashMap 作为缓存容器它支持多个线程同时读写不会出现 HashMap 在并发 resize 时可能产生的死循环问题。ConcurrentHashMap 的 get 操作性能很高适合读多写少的场景。但是ConcurrentHashMap 只能保证单个操作的原子性不能保证复合操作的原子性。我们的工厂中有一个先查缓存、未命中再加载创建、最后写入缓存的过程。这个过程可能会在并发场景下出现多个线程同时创建同一个策略对象的情况。虽然不会导致数据错误因为最终都会写入同一个 key后写入的会覆盖先写入的但会造成一定程度的重复创建开销。对于策略对象创建成本较高的场景可以使用 computeIfAbsent 结合原子操作来避免重复创建public static DiscountStrategy getStrategy(String strategyCode) { return STRATEGY_CACHE.computeIfAbsent(strategyCode, code - { String className StrategyConfig.getClassName(code); if (className null) { throw new IllegalArgumentException(未找到策略编码对应的实现 code); } try { Class? strategyClass Class.forName(className); return (DiscountStrategy) strategyClass.getDeclaredConstructor().newInstance(); } catch (Exception e) { throw new IllegalStateException(创建策略实例失败 className, e); } }); }computeIfAbsent 会在 key 不存在时原子地执行映射函数其他并发请求会等待或者拿到已经计算完成的结果从而避免重复创建。这样既保持了线程安全又避免了重复实例化带来的开销。需要注意的是computeIfAbsent 中的映射函数不应该修改缓存中的其他 key也不应该执行耗时过长的操作防止长时间占用并发控制资源。对于策略加载这种轻量操作通常没有问题。十五、异常处理与健壮性设计策略模式的反射工厂虽然优雅但反射机制会把一部分错误从编译期延迟到运行期。例如配置中写错类名、类没有无参构造方法、类路径下缺少对应 class 文件等都可能在运行时才暴露。因此健壮的错误处理非常重要。在工厂中我们对 ClassNotFoundException 和 Exception 分别进行了捕获并抛出带有清晰信息的 IllegalStateException。这样排查问题时能够快速知道是哪个策略编码、哪个类名出了问题。同时我们还应该对配置加载过程做健壮处理。如果配置文件不存在static 代码块中可以只记录警告而不抛出致命错误吗通常不建议因为策略配置是系统正常工作的根基缺失配置应当尽早暴露。但为了开发调试方便可以提供默认配置或者默认策略作为兜底。另一种常见问题是策略编码传空。工厂中需要对 null 或者空字符串进行校验避免将无效参数带入后续逻辑。传空通常意味着上游调用出现问题早抛出异常比返回 null 更利于定位问题。某些团队还会在工厂初始化时对所有策略进行预热加载这样能够在系统启动阶段就发现配置缺失或类加载失败而不是等到线上真正访问时才暴露。预热加载的实现通常是把配置文件中的类名全部加载一遍并放入缓存如发现异常则启动失败。十六、性能优化懒加载、缓存与预热工厂目前使用的是懒加载策略也就是第一次用到某个策略时才创建对象。懒加载可以缩短应用启动时间但会把首次创建的开销转移到第一个请求上可能导致首次请求耗时略高。如果策略类数量不多创建成本也不高懒加载的开销可以忽略。但如果策略类数量较多或者某些策略的初始化逻辑较重可以使用预热加载。预热加载在系统启动时遍历所有配置项提前创建并缓存所有策略对象这样启动后第一次请求就不用再承担创建开销。预热加载的另一个好处是能够提前发现配置错误。因为所有策略都会在启动时被创建一遍如果某个类名写错或类不存在系统会在启动阶段直接失败开发人员可以立即发现而不是等线上用户触发相关功能时才暴露。预热加载的实现可以参考下面的代码import java.util.Map; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; public class DiscountStrategyFactory { private static final MapString, DiscountStrategy STRATEGY_CACHE new ConcurrentHashMap(); static { Properties properties StrategyConfig.getProperties(); for (String code : properties.stringPropertyNames()) { String className properties.getProperty(code); try { Class? strategyClass Class.forName(className); DiscountStrategy strategy (DiscountStrategy) strategyClass.getDeclaredConstructor().newInstance(); STRATEGY_CACHE.put(code, strategy); } catch (Exception e) { throw new ExceptionInInitializerError(初始化策略失败 className); } } } public static DiscountStrategy getStrategy(String strategyCode) { DiscountStrategy strategy STRATEGY_CACHE.get(strategyCode); if (strategy null) { throw new IllegalArgumentException(未找到策略编码对应的实现 strategyCode); } return strategy; } }这种预热模式在中小规模项目中非常实用。策略数量通常不会很大启动开销可以接受换来的是运行时稳定和更早发现问题。十七、配置驱动的高级玩法配置文件并不只是简单的编码到类名的映射它还可以承载更多策略相关的元数据。例如可以为每个策略配置优先级、是否启用、描述信息、参数等。工厂在加载时可以同时读取这些元数据并在运行时根据条件筛选可用策略。一个更通用的配置文件可能长这样strategy.NORMAL.classcom.example.strategy.NormalDiscountStrategy strategy.NORMAL.enabledtrue strategy.NORMAL.priority100 strategy.VIP.classcom.example.strategy.VipDiscountStrategy strategy.VIP.enabledtrue strategy.VIP.priority90 strategy.FULL_REDUCTION.classcom.example.strategy.FullReductionDiscountStrategy strategy.FULL_REDUCTION.enabledtrue strategy.FULL_REDUCTION.priority80这样工厂不仅负责创建对象还可以根据 enabled 决定是否暴露该策略根据 priority 决定执行顺序。如果业务中存在多个策略组合的情况比如用户同时满足会员和满减条件我们可以按照优先级依次应用策略或者选择最优结果。策略组合是一个复杂的话题单靠一个策略对象可能不够。实践中可以把策略分为可叠加策略和互斥策略或者引入责任链模式让多个策略依次处理。策略模式加反射工厂是基础更复杂的业务规则可以在其上进行扩展。十八、与 Spring 等容器集成在 Java 后端项目中Spring 是使用最广泛的框架。策略模式和反射工厂也可以与 Spring 容器良好结合。最简单的做法是仍然使用独立的反射工厂策略类不由 Spring 管理而是由工厂自行创建。如果策略类不需要依赖其他 Bean这种方式完全可行。如果策略类需要依赖 Spring 管理的 Bean比如数据库查询服务、缓存服务等那么手动 new 创建的对象就无法享受依赖注入。解决方式有几种一种是把策略类也注册为 Spring Bean然后通过 ApplicationContext 获取另一种是在策略对象创建后利用 Spring 的 AutowireCapableBeanFactory 手动完成注入。借助 Spring 的 ApplicationContext可以更优雅地实现策略发现。例如import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; import java.util.Map; Component public class SpringStrategyFactory { private final MapString, DiscountStrategy strategyMap; public SpringStrategyFactory(ApplicationContext context, MapString, DiscountStrategy strategyBeans) { this.strategyMap strategyBeans; } public DiscountStrategy getStrategy(String code) { DiscountStrategy strategy strategyMap.get(code); if (strategy null) { throw new IllegalArgumentException(未找到策略 code); } return strategy; } }在 Spring 中如果策略实现类都使用 Component 注解且 bean 名正好是策略编码那么可以通过依赖注入 Map 一次性拿到所有策略 Bean省去反射操作。这样编译期类型检查更强也更容易调试。不过这种方式的扩展性略低于配置文件驱动因为新增策略时需要保证 Bean 名与业务编码一致。很多实际项目会把两者结合默认使用 Spring Bean Map 管理策略当新增策略时通过配置文件或注册中心控制启用状态。反射工厂则作为非 Spring 环境或轻量模块的兜底方案。十九、注解驱动的策略发现除了配置文件我们还可以通过自定义注解来标记策略实现类然后在系统启动时扫描指定包路径自动发现所有策略类并注册到工厂中。这样新增策略时只需要编写类并打上注解连配置文件都不用改。定义一个策略注解import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; Target(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) public interface StrategyType { String value(); }在具体策略类上使用注解StrategyType(VIP) public class VipDiscountStrategy implements DiscountStrategy { Override public BigDecimal calculate(Order order) { return order.getOriginalAmount().multiply(new BigDecimal(0.85)); } Override public String getStrategyCode() { return VIP; } }然后通过 classpath 扫描框架比如 Reflections 或者 Spring 的 ClassPathScanningCandidateComponentProvider识别出所有带 StrategyType 注解的类并注册。这种方式配置量更少但对扫描性能、类路径管理有一定要求适合规模较大且约定明确的系统。注解驱动的方案仍然离不开策略接口和工厂。它只是把策略注册的工作从手工配置变成了自动扫描。从开闭原则的角度看新增策略类时仍然不需要修改工厂和调用方只是新增一个类并打注解即可。二十、常见误区与设计陷阱策略模式加反射工厂虽然好用但在实践中也存在一些常见误区如果忽略可能会让这套设计沦为表面文章。第一个误区是过度设计。如果业务中的变化点非常少只有两三种固定策略而且短期内不会有太多变化引入反射工厂反而会增加系统复杂度和调试成本。设计模式应该服务于真实变化而不是为了用而用。第二个误区是策略接口膨胀。把过多不相关的方法塞进同一个策略接口会导致很多策略类被迫实现自己不需要的方法。此时应该考虑按能力拆分多个接口或者使用适配器模式让接口保持最小化。第三个误区是反射工厂返回裸接口没有做异常兜底。配置错误、类加载失败等问题被隐藏线上出现问题时很难定位。健壮的错误处理和日志记录必不可少。第四个误区是缓存的策略对象包含了可变状态。如果策略实现类内部维护了与请求相关的可变字段多个线程共享同一个实例会引发数据混乱。策略对象应该尽量设计为无状态如果需要状态应该放在方法参数或每次创建的实例中。第五个误区是只关注策略而忽略配置管理。策略配置散落在不同环境中或者缺少版本控制会导致上线后策略行为与预期不一致。配置应该纳入代码管理并且有明确的变更流程。二十一、与简单工厂、工厂方法、抽象工厂的对比很多同学容易把反射工厂和简单工厂、工厂方法、抽象工厂搞混。简单工厂通常在一个工厂类中用条件判断来创建不同对象工厂方法通过定义抽象创建方法让子类决定具体创建哪个对象抽象工厂则创建一组相关对象。反射工厂与简单工厂最核心的区别在于简单工厂创建对象时通常需要显式写出 new而反射工厂通过类名字符串动态创建新增类时可以不修改工厂代码。简单工厂如果要新增产品必须修改工厂里的判断逻辑违反开闭原则反射工厂则可以把扩展放在配置或扫描中。反射工厂也可以看作简单工厂的一种动态实现。它把创建规则从硬编码逻辑转移到了可配置的元数据上。与工厂方法相比反射工厂不需要为每个产品建立工厂子类扩展起来更加轻量。与抽象工厂相比反射工厂更聚焦于单个产品族的创建不会试图同时创建一组相关对象。在实际项目中我们未必只能选择一种。很多时候是简单工厂起步当变化增多后再升级为反射工厂。理解它们之间的差异有助于根据当前系统的复杂度和变化趋势做出合适的技术选型。二十二、完整项目示例从零搭建为了帮助大家快速落地我们把前面涉及的关键类汇总成一个完整的项目结构并给出核心目录布局。假设项目包名为 com.example.strategy结构如下src/main/java/com/example/strategy ├── Order.java ├── DiscountStrategy.java ├── NormalDiscountStrategy.java ├── VipDiscountStrategy.java ├── FullReductionDiscountStrategy.java ├── FestivalDiscountStrategy.java ├── NewUserDiscountStrategy.java ├── DiscountStrategyFactory.java ├── StrategyConfig.java └── OrderService.java src/main/resources └── strategy-config.properties核心文件上文已经给出这里不再重复。我们只需要再补充一个简单的测试类来验证策略工厂的效果import java.math.BigDecimal; public class OrderServiceTest { public static void main(String[] args) { OrderService orderService new OrderService(); Order vipOrder new Order(new BigDecimal(200), VIP, VIP); BigDecimal vipPay orderService.calculatePayAmount(vipOrder); System.out.println(VIP 订单应付金额 vipPay); Order reductionOrder new Order(new BigDecimal(350), NORMAL, FULL_REDUCTION); BigDecimal reductionPay orderService.calculatePayAmount(reductionOrder); System.out.println(满减订单应付金额 reductionPay); } }运行后VIP 订单会输出 170.0满减订单会输出 300.0符合预期。这个示例虽然简单但已经完整展示了策略模式、反射工厂、配置驱动和客户端调用的完整链路。读者可以在这个基础上继续扩展比如增加优惠券策略、拼团策略、限时抢购策略或者把配置改为数据库存储或者集成到真实订单系统中进行测试。核心思路不变抽象稳定接口隔离具体变化动态创建对象关闭已有代码修改。二十三、总结与最佳实践策略模式加反射工厂是一种非常实用的组合它把“做什么”和“怎么做”进行了分离并进一步把“如何创建”从“如何选择”中解放出来。通过策略接口定义稳定扩展点通过反射工厂完成动态对象创建再结合配置文件或注解驱动注册系统在面对业务变化时可以更加从容。这套方案的核心价值并不是炫技而是让变化被限制在最小的范围内。新增策略时我们不修改已经稳定的核心代码修改已有策略时不会影响其他策略和调用方。这让开发人员能够并行工作、独立测试、快速上线也让代码的长期维护成本大大降低。最后总结几条最佳实践。第一策略接口要尽量小而稳定避免因为接口变动导致大量实现类被迫修改。第二策略对象尽量设计为无状态方便缓存和复用。第三工厂必须做好缓存、线程安全和异常处理。第四配置要集中管理、纳入版本控制并建立清晰的变更流程。第五根据项目规模和变化频率选择合适方案不要为了设计模式而过度设计。当你面对大量 if-else、switch-case 或频繁变动的业务规则时不妨尝试用策略模式加反射工厂进行重构它会帮助你构建一个更清晰、更可扩展、更符合开闭原则的系统。
返回列表