策略模式 + 反射工厂:优雅实现开闭原则的终极指南
1. 引言
在软件开发的漫长历史中,如何构建易于维护、易于扩展的系统始终是工程师们追求的核心目标。设计模式作为解决特定问题的经典方案,为我们提供了无数经过验证的架构思路。而在所有设计原则中,开闭原则(Open-Closed Principle, OCP)被誉为面向对象设计的“基石”,它指导我们如何设计出对扩展开放、对修改关闭的模块。
然而,单纯的理论往往难以落地。在实际项目中,我们经常面临这样的场景:业务规则频繁变化,算法需要动态切换,而每次修改都意味着对既有代码的改动,容易引入缺陷,破坏系统的稳定性。此时,策略模式(Strategy Pattern)与反射工厂(Reflective Factory)的结合,成为了一种极其优雅且强大的解决方案。它不仅完美诠释了开闭原则,还能让系统在复杂业务迭代中保持健壮与灵活。
本文将用近2万字的篇幅,从基础概念到高级应用,深入剖析策略模式与反射工厂的精髓,并通过大量代码示例和实践经验,带你领略这一组合的无穷魅力。无论你是初学者还是资深架构师,都能从中获得启发。
2. 开闭原则(Open-Closed Principle)
2.1 定义与起源
开闭原则最早由 Bertrand Meyer 在 1988 年提出,其核心思想是:
软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
通俗地说,就是当系统需求发生变化时,我们应当尽量通过增加新代码来满足变化,而不是修改已有的、经过测试的代码。这样既能保证原有功能的稳定性,又能快速响应业务变化。
2.2 为什么需要开闭原则?
降低风险:修改已有代码可能引入新的缺陷,尤其是核心模块。遵循 OCP 可以减少修改,从而降低风险。
提高可维护性:新功能的添加独立于旧代码,模块职责清晰,易于理解和维护。
增强复用性:稳定的模块可以在多个项目中复用,不受具体场景变化的影响。
便于测试:新增的扩展点可以独立测试,无需重复验证原有功能。
2.3 实现开闭原则的常见手段
抽象与多态:通过接口或抽象类定义抽象层,让具体实现依赖于抽象,从而可以方便地替换实现。
依赖注入:将依赖对象的创建和使用分离,由外部容器负责注入,降低耦合。
设计模式:如策略模式、模板方法模式、观察者模式等,都天然符合 OCP。
反射与配置:利用反射机制动态加载类,结合配置文件或注解,实现零修改扩展。
在本文中,我们将重点讨论最后一种方式——反射与策略模式的结合。
3. 策略模式(Strategy Pattern)
3.1 定义与意图
策略模式属于行为型设计模式,其定义如下:
定义一系列算法,将每一个算法封装起来,并使它们可以相互替换。策略模式让算法的变化独立于使用算法的客户。
通俗解释:当一个系统有多种处理方式,且需要根据条件动态选择时,我们可以将每种处理方式封装成独立的策略类,然后在运行时选择使用哪一种策略。
3.2 模式结构
策略模式包含三个核心角色:
Context(上下文):持有对策略对象的引用,负责调用策略的具体方法。客户端通常只与 Context 交互。
Strategy(抽象策略):定义所有策略类必须实现的公共接口,通常是一个接口或抽象类。
ConcreteStrategy(具体策略):实现 Strategy 接口,提供具体的算法实现。
下面是一个简单的 UML 类图描述(文字版):
text
+----------------+ +------------------+ | Context | | <<interface>> | | |--------->| Strategy | | - strategy: Strategy |------------------| | + setStrategy(Strategy) | + execute() | | + execute() | | +----------------+ +------------------+ ^ | +---------------+---------------+ | | | +---------+----+ +--------+------+ +----+----------+ | ConcreteStrategyA| | ConcreteStrategyB| | ConcreteStrategyC| | + execute() | | + execute() | | + execute() | +------------------+ +------------------+ +------------------+
3.3 代码示例:电商促销策略
假设我们有一个电商系统,需要根据不同的促销活动计算订单的最终价格。我们可以定义折扣策略接口,以及多种具体策略(如无折扣、满减、打折)。
3.3.1 抽象策略接口
java
public interface DiscountStrategy { double calculate(double originalPrice); }3.3.2 具体策略实现
java
// 无折扣 public class NoDiscountStrategy implements DiscountStrategy { @Override public double calculate(double originalPrice) { return originalPrice; } } // 固定打折,比如8折 public class PercentageDiscountStrategy implements DiscountStrategy { private double percentage; // 0.8 表示8折 public PercentageDiscountStrategy(double percentage) { this.percentage = percentage; } @Override public double calculate(double originalPrice) { return originalPrice * percentage; } } // 满减,如满100减20 public class ThresholdDiscountStrategy implements DiscountStrategy { private double threshold; private double discount; public ThresholdDiscountStrategy(double threshold, double discount) { this.threshold = threshold; this.discount = discount; } @Override public double calculate(double originalPrice) { if (originalPrice >= threshold) { return originalPrice - discount; } return originalPrice; } }3.3.3 上下文(Context)
java
public class Order { private double price; private DiscountStrategy discountStrategy; public Order(double price) { this.price = price; } public void setDiscountStrategy(DiscountStrategy discountStrategy) { this.discountStrategy = discountStrategy; } public double calculateFinalPrice() { if (discountStrategy == null) { throw new IllegalStateException("请先设置折扣策略"); } return discountStrategy.calculate(price); } }3.3.4 客户端使用
java
public class Client { public static void main(String[] args) { Order order = new Order(200); // 使用8折策略 order.setDiscountStrategy(new PercentageDiscountStrategy(0.8)); System.out.println("8折后价格: " + order.calculateFinalPrice()); // 160 // 切换为满减策略 order.setDiscountStrategy(new ThresholdDiscountStrategy(100, 20)); System.out.println("满减后价格: " + order.calculateFinalPrice()); // 180 } }3.4 策略模式的优缺点
优点:
符合开闭原则:新增策略无需修改原有代码,只需添加新策略类并让客户端选择。
避免多重条件语句:消除了 if-else 或 switch-case,代码更清晰。
策略可以复用:多个 Context 可以共享同一个策略对象。
缺点:
客户端必须了解不同策略的区别:客户端需要知道有哪些策略以及如何选择,增加了客户端的复杂性。
策略类数量增多:每个策略一个类,可能导致类爆炸。
策略之间的切换成本:如果策略是带状态的,切换时可能需要额外处理。
4. 工厂模式与反射
4.1 工厂模式简介
工厂模式用于封装对象的创建过程,将客户端与具体类的实例化解耦。常见的工厂模式有简单工厂、工厂方法和抽象工厂。这里我们主要关注简单工厂和工厂方法,因为它们常与策略模式结合使用,用于创建策略对象。
简单工厂示例:
java
public class DiscountStrategyFactory { public static DiscountStrategy getStrategy(String type) { switch (type) { case "none": return new NoDiscountStrategy(); case "percentage": return new PercentageDiscountStrategy(0.8); case "threshold": return new ThresholdDiscountStrategy(100, 20); default: throw new IllegalArgumentException("未知策略类型"); } } }但这种传统工厂存在一个问题:每增加一个新的策略,都需要修改工厂类的代码,违反了开闭原则。
4.2 反射技术概述
反射(Reflection)是 Java、C# 等语言提供的一种能力,允许程序在运行时检查或修改类的行为。通过反射,我们可以动态地获取类信息、创建对象、调用方法,甚至访问私有成员。
Java 反射常用 API:
Class.forName(String className):加载指定类名的 Class 对象。clazz.newInstance():调用无参构造器创建实例(已过时,推荐使用clazz.getDeclaredConstructor().newInstance())。clazz.getMethod()等获取方法并调用。
反射使得我们可以在运行时根据字符串类名创建对象,这正是实现动态扩展的关键。
4.3 反射工厂的实现
反射工厂的核心思想是:将策略类的类型标识与类全限定名映射起来,通过反射动态创建实例,从而避免修改工厂代码。
4.3.1 基本反射工厂
java
import java.util.Properties; public class ReflectDiscountStrategyFactory { private static Properties strategyProps = new Properties(); static { // 通常从配置文件加载映射关系 strategyProps.setProperty("none", "com.example.strategy.NoDiscountStrategy"); strategyProps.setProperty("percentage", "com.example.strategy.PercentageDiscountStrategy"); strategyProps.setProperty("threshold", "com.example.strategy.ThresholdDiscountStrategy"); } public static DiscountStrategy getStrategy(String type) { String className = strategyProps.getProperty(type); if (className == null) { throw new IllegalArgumentException("未找到策略类型: " + type); } try { Class<?> clazz = Class.forName(className); return (DiscountStrategy) clazz.getDeclaredConstructor().newInstance(); } catch (Exception e) { throw new RuntimeException("创建策略对象失败", e); } } }这样,当增加新策略时,我们只需添加一个新的策略类,并在配置文件(或 Properties)中增加映射,工厂无需修改。完全符合开闭原则。
4.3.2 支持带参数的构造函数
上面的例子仅支持无参构造。如果策略需要参数(如百分比折扣需要折扣率),我们可以通过反射传递参数,但这会复杂化工厂。一种常见做法是让策略类提供无参构造,并通过 setter 或配置属性注入参数。或者使用更高级的依赖注入框架。
5. 策略模式 + 反射工厂:珠联璧合
5.1 为什么要结合?
策略模式本身已经符合开闭原则,因为添加新策略不需要修改 Context 和原有策略。但策略的创建过程却可能成为破坏 OCP 的源头。如果客户端直接new具体策略,那么当新增策略时,客户端代码仍然需要修改。即使使用简单工厂,工厂类也需要修改。
因此,我们需要一种机制,使得策略的创建也无需修改已有代码。反射工厂正好解决了这个问题:通过配置或注解动态加载策略类,实现了策略对象的“零修改”创建。
5.2 结合后的架构
结合后的系统包含以下组件:
策略接口:定义行为。
具体策略类:实现策略接口,每个类独立。
策略工厂(反射版):负责根据标识动态创建策略实例。
上下文:持有策略引用,可能通过工厂获取策略。
配置源:如 properties、XML、注解,保存标识与策略类的映射。
客户端:通过标识向工厂请求策略,设置给上下文。
架构图(文字描述):
text
客户端 -> 策略工厂 (反射) <-- 读取 --> 配置文件 | | | v | 创建策略实例 v | 上下文 <-------+ | v 执行策略
5.3 如何实现开闭原则?
对扩展开放:新增业务规则只需添加一个新的策略类,并在配置文件中增加一行映射。无需修改任何现有代码。
对修改关闭:原有的策略类、工厂类、上下文类、客户端代码均无需改动(除非客户端需要感知新策略类型,但通常客户端通过配置或用户选择动态传入类型)。
这样,整个系统对变化是完全封闭的,而对扩展是完全开放的。
6. 实战案例:构建灵活的验证框架
为了更深入地展示策略模式+反射工厂的威力,我们将构建一个通用的表单验证框架。该框架可以根据不同的验证规则(如非空、长度限制、正则表达式、自定义校验)对输入数据进行验证,并且能够轻松扩展新的验证规则。
6.1 需求分析
假设我们有一个用户注册表单,包含字段:用户名、密码、邮箱、手机号。我们需要对每个字段应用不同的验证规则:
用户名:非空,长度3-20。
密码:非空,长度6-18。
邮箱:非空,符合邮箱格式。
手机号:可选,如果填写则需符合手机号格式。
随着业务发展,可能会增加新字段(如年龄、地址)或新规则(如密码复杂度、唯一性检查)。我们需要保证框架的可扩展性。
6.2 设计策略接口
定义验证策略接口ValidationStrategy:
java
public interface ValidationStrategy { /** * 执行验证 * @param value 待验证的值 * @return 验证结果,包含是否成功和错误信息 */ ValidationResult validate(Object value); }验证结果类:
java
public class ValidationResult { private boolean valid; private String message; // 构造器、getter/setter public static ValidationResult ok() { return new ValidationResult(true, null); } public static ValidationResult fail(String message) { return new ValidationResult(false, message); } // ... 省略具体实现 }6.3 实现具体策略
6.3.1 非空验证
java
public class NotNullStrategy implements ValidationStrategy { @Override public ValidationResult validate(Object value) { if (value == null || (value instanceof String && ((String) value).trim().isEmpty())) { return ValidationResult.fail("该字段不能为空"); } return ValidationResult.ok(); } }6.3.2 长度验证
java
public class LengthStrategy implements ValidationStrategy { private int min; private int max; // 必须提供无参构造,以便反射创建;参数通过 setter 或额外方式注入 public LengthStrategy() { // 默认值,可通过配置覆盖 this.min = 0; this.max = Integer.MAX_VALUE; } public void setMin(int min) { this.min = min; } public void setMax(int max) { this.max = max; } @Override public ValidationResult validate(Object value) { if (value == null) { return ValidationResult.fail("输入值为空"); } String str = value.toString(); int len = str.length(); if (len < min || len > max) { return ValidationResult.fail("长度必须在 " + min + " 到 " + max + " 之间"); } return ValidationResult.ok(); } }6.3.3 正则验证
java
public class RegexStrategy implements ValidationStrategy { private String pattern; public RegexStrategy() { } public void setPattern(String pattern) { this.pattern = pattern; } @Override public ValidationResult validate(Object value) { if (value == null) { return ValidationResult.fail("输入值为空"); } if (!value.toString().matches(pattern)) { return ValidationResult.fail("格式不正确"); } return ValidationResult.ok(); } }6.3.4 手机号验证(复用正则)
我们可以专门定义一个手机号策略,继承自 RegexStrategy 并预设 pattern。
java
public class MobileStrategy extends RegexStrategy { public MobileStrategy() { setPattern("^1[3-9]\\d{9}$"); } }6.4 反射工厂与配置
6.4.1 策略工厂
我们需要一个工厂,能够根据策略名称创建策略实例,并且支持向策略注入参数(如 min/max/pattern)。这里我们采用更灵活的设计:从配置中读取策略的类名和参数,然后通过反射设置属性。
首先定义策略配置的数据结构:
java
public class StrategyConfig { private String name; // 策略标识,如 "notNull" private String className; // 策略类全名 private Map<String, Object> params; // 参数键值对 // getter/setter ... }然后实现反射工厂:
java
import java.lang.reflect.Method; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class ValidationStrategyFactory { // 缓存策略配置,实际可从配置文件加载 private static Map<String, StrategyConfig> configMap = new ConcurrentHashMap<>(); static { // 模拟从配置文件加载 loadConfig(); } private static void loadConfig() { // 通常解析 properties、XML 或 JSON 文件 // 这里硬编码演示 StrategyConfig notNull = new StrategyConfig("notNull", "com.example.validation.NotNullStrategy", null); configMap.put("notNull", notNull); StrategyConfig length = new StrategyConfig("length", "com.example.validation.LengthStrategy", new HashMap<>()); length.getParams().put("min", 3); length.getParams().put("max", 20); configMap.put("length", length); StrategyConfig regex = new StrategyConfig("regex", "com.example.validation.RegexStrategy", new HashMap<>()); regex.getParams().put("pattern", "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"); configMap.put("email", regex); // 邮箱复用正则策略 StrategyConfig mobile = new StrategyConfig("mobile", "com.example.validation.MobileStrategy", null); configMap.put("mobile", mobile); } public static ValidationStrategy getStrategy(String name) { StrategyConfig config = configMap.get(name); if (config == null) { throw new IllegalArgumentException("未知策略: " + name); } try { Class<?> clazz = Class.forName(config.getClassName()); Object instance = clazz.getDeclaredConstructor().newInstance(); // 注入参数 if (config.getParams() != null) { for (Map.Entry<String, Object> entry : config.getParams().entrySet()) { String paramName = entry.getKey(); Object paramValue = entry.getValue(); // 寻找 setter 方法,例如 setMin String setterName = "set" + paramName.substring(0, 1).toUpperCase() + paramName.substring(1); Method setter = findSetter(clazz, setterName, paramValue.getClass()); if (setter != null) { setter.invoke(instance, paramValue); } else { // 尝试类型转换,比如 String 转 int // 简化处理,实际可以使用类型转换器 } } } return (ValidationStrategy) instance; } catch (Exception e) { throw new RuntimeException("创建策略失败", e); } } private static Method findSetter(Class<?> clazz, String setterName, Class<?> paramType) { try { return clazz.getMethod(setterName, paramType); } catch (NoSuchMethodException e) { // 尝试父类 return null; } } }6.4.2 配置文件示例(properties)
我们可以将配置外移到文件,比如strategies.properties:
text
notNull.class=com.example.validation.NotNullStrategy length.class=com.example.validation.LengthStrategy length.params.min=3 length.params.max=20 email.class=com.example.validation.RegexStrategy email.params.pattern=^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$ mobile.class=com.example.validation.MobileStrategy工厂启动时加载此文件。
6.5 上下文:字段验证器
现在定义字段验证器FieldValidator,它包含字段名、字段值以及要应用的策略列表。
java
import java.util.ArrayList; import java.util.List; public class FieldValidator { private String fieldName; private Object fieldValue; private List<String> strategyNames = new ArrayList<>(); public FieldValidator(String fieldName, Object fieldValue) { this.fieldName = fieldName; this.fieldValue = fieldValue; } public void addStrategy(String strategyName) { strategyNames.add(strategyName); } public List<ValidationResult> validate() { List<ValidationResult> results = new ArrayList<>(); for (String name : strategyNames) { ValidationStrategy strategy = ValidationStrategyFactory.getStrategy(name); ValidationResult result = strategy.validate(fieldValue); if (!result.isValid()) { // 可以添加字段信息 result.setMessage(fieldName + ": " + result.getMessage()); } results.add(result); } return results; } }6.6 客户端使用示例
java
public class RegistrationForm { public static void main(String[] args) { // 模拟表单数据 String username = "john"; String password = "123456"; String email = "john@example.com"; String mobile = "13800138000"; // 验证用户名:非空,长度3-20 FieldValidator usernameValidator = new FieldValidator("用户名", username); usernameValidator.addStrategy("notNull"); usernameValidator.addStrategy("length"); // 默认 min=3, max=20 // 验证密码:非空,长度6-18 FieldValidator passwordValidator = new FieldValidator("密码", password); passwordValidator.addStrategy("notNull"); // 密码长度我们想用6-18,但 length 策略默认配置是3-20,需要重新配置?我们可以定义新的策略配置,或允许传入参数。 // 为了演示,假设我们有一个 passwordLength 策略,配置为6-18。 // 验证邮箱:非空,正则 FieldValidator emailValidator = new FieldValidator("邮箱", email); emailValidator.addStrategy("notNull"); emailValidator.addStrategy("email"); // 验证手机:可选,如果有值则验证格式 FieldValidator mobileValidator = new FieldValidator("手机号", mobile); mobileValidator.addStrategy("mobile"); // 手机号策略内部有正则 // 执行验证 List<ValidationResult> results = new ArrayList<>(); results.addAll(usernameValidator.validate()); results.addAll(passwordValidator.validate()); results.addAll(emailValidator.validate()); results.addAll(mobileValidator.validate()); // 输出结果 for (ValidationResult result : results) { if (!result.isValid()) { System.out.println(result.getMessage()); } } } }6.7 扩展新验证规则
假设新增需求:密码必须包含数字和字母。我们只需添加一个新的策略类:
java
public class PasswordComplexityStrategy implements ValidationStrategy { @Override public ValidationResult validate(Object value) { if (value == null) return ValidationResult.fail("密码不能为空"); String pwd = value.toString(); boolean hasDigit = pwd.matches(".*\\d.*"); boolean hasLetter = pwd.matches(".*[a-zA-Z].*"); if (!hasDigit || !hasLetter) { return ValidationResult.fail("密码必须包含数字和字母"); } return ValidationResult.ok(); } }然后在配置文件添加:
text
passwordComplexity.class=com.example.validation.PasswordComplexityStrategy
客户端只需使用passwordValidator.addStrategy("passwordComplexity"),无需修改任何已有代码。完美符合开闭原则。
7. 深入分析与讨论
7.1 反射的性能考量
反射虽然强大,但性能开销不容忽视。每次通过反射创建对象都会涉及类加载、安全检查、方法调用等,比直接new慢得多。在高并发场景下,如果频繁创建策略对象,可能成为瓶颈。
优化策略:
缓存反射创建的对象:如果策略是无状态的,可以创建单例,从缓存中返回同一个实例。
使用对象池:对有状态的策略,可以使用对象池复用实例。
结合 Spring 等 IoC 容器:让容器管理策略 Bean,通过容器获取实例,既利用了反射又利用了容器的缓存和 AOP 能力。
使用编译时代理:如 Java 的
MethodHandle或字节码生成技术(如 CGLib),比纯反射略快。
在我们的验证框架中,策略大多是无状态的(LengthStrategy 虽然有 min/max,但这些参数一旦设置就固定,可以视为有状态但不可变),我们可以缓存创建好的策略实例。
改进工厂:增加策略缓存。
java
public class ValidationStrategyFactory { private static Map<String, ValidationStrategy> strategyCache = new ConcurrentHashMap<>(); public static ValidationStrategy getStrategy(String name) { // 先查缓存 if (strategyCache.containsKey(name)) { return strategyCache.get(name); } // 否则创建并缓存 ValidationStrategy strategy = createStrategy(name); strategyCache.put(name, strategy); return strategy; } // ... createStrategy 方法内部使用反射创建 }7.2 策略注册方式的选择
除了配置文件(properties/XML),还有其他方式可以实现策略的注册:
注解(Annotation):在策略类上使用自定义注解,标注策略标识。工厂启动时通过扫描包获取所有标注的类,建立映射。这种方式去除了配置文件,更加内聚,但需要编译时确定。
示例注解:
java
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface Strategy { String value(); // 策略标识 }在策略类上标注:
java
@Strategy("notNull") public class NotNullStrategy implements ValidationStrategy { ... }工厂通过类路径扫描(如使用 Reflections 库)收集所有带有@Strategy注解的类,注册到映射中。
SPI(Service Provider Interface):利用 Java 的
ServiceLoader机制,在META-INF/services下放置接口全名文件,内容为实现类。然后通过ServiceLoader.load加载所有实现。这种方式适合插件化架构。依赖注入容器:如 Spring,通过配置 Bean 或组件扫描,将策略类注入到 Map 中,根据 Bean 名称获取。
每种方式各有优缺点,需根据项目规模、技术栈和团队偏好选择。
7.3 与策略模式相关的其他模式
状态模式:与策略模式结构相似,但意图不同。状态模式通过改变对象内部状态来改变行为,状态之间通常有关联;策略模式是算法族的替换,策略之间独立。
模板方法模式:定义算法骨架,子类实现某些步骤。可以与策略结合,比如模板方法定义验证流程,策略实现具体验证逻辑。
工厂方法模式:将对象的创建延迟到子类。可以用工厂方法创建策略,但通常结合反射工厂更灵活。
组合模式:可以将多个策略组合成一个复合策略(如 AND 或 OR 逻辑),实现更复杂的规则。
7.4 开闭原则的局限性与注意事项
虽然策略模式+反射工厂很好地实现了 OCP,但在实际应用中仍需注意:
过度设计:如果业务规则极少变化,使用反射工厂可能增加不必要的复杂度。应遵循 YAGNI 原则。
配置管理:配置文件可能分散,需要统一管理,确保正确性。
错误处理:反射可能抛出多种异常,需要妥善处理,给出友好的错误提示。
类型安全:反射绕过了编译期类型检查,可能引入运行时错误。通过单元测试覆盖可以降低风险。
性能开销:如前所述,需合理缓存。
8. 最佳实践与实战经验
8.1 项目结构建议
在大型项目中,策略类可能很多,建议按模块组织:
text
com.example.project ├── strategy │ ├── annotation │ │ └── Strategy.java │ ├── base │ │ └── ValidationStrategy.java │ ├── impl │ │ ├── NotNullStrategy.java │ │ ├── LengthStrategy.java │ │ └── ... │ └── factory │ ├── StrategyFactory.java │ └── StrategyRegistry.java ├── config │ └── strategy-config.xml └── context └── FieldValidator.java
8.2 结合 Spring 框架
如果项目使用 Spring,可以大大简化策略的注册和获取。
8.2.1 通过 @Component 自动注册
java
@Component("notNull") public class NotNullStrategy implements ValidationStrategy { ... } @Component("length") public class LengthStrategy implements ValidationStrategy { ... }在工厂中注入所有策略 Bean:
java
@Component public class SpringStrategyFactory { @Autowired private Map<String, ValidationStrategy> strategyMap; // key为bean名称 public ValidationStrategy getStrategy(String name) { ValidationStrategy strategy = strategyMap.get(name); if (strategy == null) { throw new IllegalArgumentException("未找到策略: " + name); } return strategy; } }Spring 会收集所有类型为ValidationStrategy的 Bean,Bean 名称默认是类名首字母小写(或通过@Component指定)。这种方式既不需要反射,也不需要手动维护配置,性能好,代码简洁。
8.2.2 使用 @Qualifier 区分
如果同一类型有多个 Bean,可以使用@Qualifier进行更精细的注入。
8.3 参数化策略的配置管理
对于带参数的策略(如 LengthStrategy),在 Spring 中可以通过@Value或@ConfigurationProperties注入参数,或者使用原型作用域(@Scope("prototype"))每次创建新实例。
如果是非 Spring 项目,可以考虑使用一个统一的参数对象传递给策略,或者使用建造者模式创建策略。
8.4 策略的组合与链式调用
有时一个字段需要应用多个策略,并且希望它们按顺序执行,遇到失败就停止(短路)。我们可以设计一个组合策略:
java
public class CompositeStrategy implements ValidationStrategy { private List<ValidationStrategy> strategies; public CompositeStrategy(List<ValidationStrategy> strategies) { this.strategies = strategies; } @Override public ValidationResult validate(Object value) { for (ValidationStrategy s : strategies) { ValidationResult result = s.validate(value); if (!result.isValid()) { return result; // 失败即返回 } } return ValidationResult.ok(); } }在工厂中,我们可以从配置中读取策略列表,组装成组合策略。这样客户端无需关心内部逻辑。
8.5 测试策略
由于策略类和工厂都是可独立测试的,建议编写单元测试覆盖每个策略的正确性,以及工厂的创建逻辑。
java
public class NotNullStrategyTest { @Test public void testNull() { NotNullStrategy strategy = new NotNullStrategy(); assertFalse(strategy.validate(null).isValid()); } @Test public void testEmptyString() { NotNullStrategy strategy = new NotNullStrategy(); assertFalse(strategy.validate("").isValid()); } @Test public void testValidString() { NotNullStrategy strategy = new NotNullStrategy(); assertTrue(strategy.validate("hello").isValid()); } }对于反射工厂,可以 mock 配置文件,测试是否能正确创建预期类型。
8.6 注意事项
避免过度反射:反射虽然灵活,但应尽量在启动时完成,减少运行时反射次数。
参数类型转换:配置文件中的参数都是字符串,需要转换成策略 setter 所需的类型。可以使用类型转换器(如 Spring 的 TypeConverter)或约定只支持基本类型。
线程安全:如果策略是有状态的(如保存了上次验证结果),则不能多线程共享。需要确保工厂返回的是新实例或使用线程封闭。
9. 总结
9.1 回顾核心思想
本文围绕“策略模式 + 反射工厂”这一组合,深入探讨了它们如何完美实现开闭原则。策略模式将算法封装成独立的类,使算法可以独立变化;反射工厂则将策略对象的创建过程与具体类解耦,通过配置或注解动态加载。两者结合,使得系统在增加新策略时,无需修改任何已有代码,达到了“对扩展开放,对修改关闭”的理想状态。
9.2 适用场景
这种组合特别适合以下场景:
业务规则频繁变化:如促销活动、折扣计算、验证规则、计费方式等。
算法族庞大且易变:需要支持多种算法,且未来可能增加。
希望实现插件化架构:允许第三方开发者扩展功能。
需要动态切换行为:运行时根据配置或用户选择切换策略。
9.3 与其他技术方案的对比
简单工厂 + 条件判断:扩展时需要修改工厂,违反 OCP。
注解扫描:无需配置文件,但需约定扫描路径,启动时略慢。
SPI:适合框架扩展,但加载所有实现,可能加载不需要的类。
依赖注入容器:最简洁,但需要容器支持。
9.4 未来展望
随着微服务、云原生的发展,策略模式+反射工厂的思想也扩展到了分布式系统。例如,可以通过配置中心动态下发策略配置,实现运行时策略切换;或者结合函数式编程,将策略视为 lambda 表达式,减少类数量。
