
Java代码审查中常见的设计模式误用与重构解决方案
大家好,作为一名在Java领域摸爬滚打了多年的开发者,我参与和主导过无数次代码审查。一个深刻的体会是:设计模式本应是提升代码质量的利器,但一旦被误用或滥用,反而会成为系统维护的噩梦。今天,我想结合自己踩过的“坑”和重构经验,聊聊代码审查中几种最常见的设计模式误用,以及如何将它们“拉回正轨”。
一、 单例模式(Singleton):从全局变量到资源管理的蜕变
单例模式恐怕是被误用最多的模式之一。审查代码时,我经常看到这样的“经典”写法:
public class GlobalConfig {
private static GlobalConfig instance = new GlobalConfig();
private Map configMap = new HashMap();
private GlobalConfig() {}
public static GlobalConfig getInstance() {
return instance;
}
// 一堆getter/setter操作这个全局Map
public void setConfig(String key, String value) {
configMap.put(key, value);
}
public String getConfig(String key) {
return configMap.get(key);
}
}
问题诊断: 这本质上是一个披着单例外衣的全局可变状态。任何类都可以通过 `GlobalConfig.getInstance().setConfig(...)` 修改配置,导致程序状态不可预测,严重破坏可测试性(测试用例之间相互干扰)。
重构方案: 单例的核心是控制实例数量,而非提供全局写入口。重构方向应是不可变性与明确依赖。
// 方案1:使用枚举实现(简洁、防反射攻击)
public enum ConfigHolder {
INSTANCE;
private final Properties properties;
ConfigHolder() {
// 在构造器(即初始化时)加载配置,之后只读
properties = loadPropertiesFromFile("config.properties");
}
public String getProperty(String key) {
return properties.getProperty(key);
}
// 注意:没有setProperty方法!
}
// 方案2:依赖注入(更优雅,适合复杂场景)
@Component // Spring等框架注解
public class AppConfig {
private final Map configMap;
@Autowired
public AppConfig(Environment env) {
// 启动时从外部环境(文件、数据库、配置中心)一次性加载
this.configMap = loadImmutableConfig(env);
}
public String getConfig(String key) {
return configMap.get(key);
}
}
实战提示: 问问自己,这个“单例”真的需要全局唯一吗?它的数据是可变的吗?优先考虑将其作为依赖注入的普通对象,让IoC容器管理其生命周期。
二、 工厂模式(Factory):避免成为“上帝类”的创造营
工厂模式的初衷是封装对象创建的复杂性。但我在审查中常看到一个庞大的 `ObjectFactory`,包含了创建系统中半数对象的 `createXxx()` 方法。
public class MonsterFactory {
public static Enemy createEnemy(String type) {
if ("goblin".equals(type)) {
return new Goblin();
} else if ("dragon".equals(type)) {
return new Dragon();
} else if ("orc".equals(type)) {
return new Orc();
}
// ...后续新增怪物类型,不断修改这个类
throw new IllegalArgumentException("Unknown enemy type");
}
}
问题诊断: 这违反了开闭原则。每次新增一种 `Enemy`,都必须修改 `MonsterFactory` 类的代码,使其变得臃肿且脆弱。
重构方案: 使用注册表或结合反射/ServiceLoader实现可扩展的工厂,或者直接升级为抽象工厂。
// 方案:使用注册表模式的工厂
public class EnemyFactory {
private static final Map<String, Supplier> registry = new HashMap();
static {
// 初始化注册已知类型
register("goblin", Goblin::new);
register("dragon", Dragon::new);
}
public static void register(String type, Supplier supplier) {
registry.put(type, supplier);
}
public static Enemy createEnemy(String type) {
Supplier supplier = registry.get(type);
if (supplier == null) {
throw new IllegalArgumentException("Unknown enemy type: " + type);
}
return supplier.get();
}
}
// 在应用启动时,新模块可以自行注册
// EnemyFactory.register("orc", Orc::new);
踩坑提示: 不要指望一个工厂解决所有创建问题。根据创建逻辑的关联性进行拆分,比如 `UserFactory`、`PaymentFactory`,或者为同一产品族使用抽象工厂。
三、 策略模式(Strategy):当条件分支“伪装”成策略
策略模式旨在将算法族封装起来,使它们可以互相替换。误用常表现为:策略接口只有一个方法,但策略的选择逻辑仍是一长串 `if-else` 或 `switch`。
public class DiscountService {
public double calculate(String userType, double price) {
DiscountStrategy strategy;
if ("VIP".equals(userType)) {
strategy = new VipDiscount();
} else if ("Regular".equals(userType)) {
strategy = new RegularDiscount();
} else {
strategy = new NoDiscount();
}
return strategy.apply(price);
}
}
问题诊断: 策略对象的创建逻辑仍然耦合在客户端代码中,新增策略仍需修改 `calculate` 方法。这仅仅是转移了算法实现,没有彻底解耦策略的“定义”和“使用”。
重构方案: 引入一个策略上下文或工厂来管理策略的映射和获取,实现策略选择的动态化。
@Component
public class DiscountStrategyFactory {
private final Map strategyMap;
@Autowired
public DiscountStrategyFactory(List strategies) {
// Spring会自动注入所有实现DiscountStrategy的Bean
strategyMap = strategies.stream()
.collect(Collectors.toMap(s -> s.getType(), Function.identity()));
}
public DiscountStrategy getStrategy(String userType) {
return strategyMap.getOrDefault(userType, new NoDiscount());
}
}
@Service
public class DiscountService {
@Autowired
private DiscountStrategyFactory factory;
public double calculate(String userType, double price) {
// 选择策略的职责移交给了工厂
DiscountStrategy strategy = factory.getStrategy(userType);
return strategy.apply(price);
}
}
// 每个策略实现可以自带标识
@Component
public class VipDiscount implements DiscountStrategy {
@Override
public String getType() {
return "VIP";
}
@Override
public double apply(double price) {
return price * 0.8;
}
}
实战感言: 真正的策略模式,其威力在于运行时动态替换算法。利用Spring的依赖注入特性,可以优雅地实现策略的自动注册和发现,让新增一个策略变成“只需新增一个实现类”那么简单。
四、 观察者模式(Observer):警惕内存泄漏与失控的链式反应
观察者模式用于建立对象间一对多的依赖。Java自身提供了 `java.util.Observable` 和 `Observer`,但它们在代码审查中往往是“红色警报”。
public class Order extends Observable { // 已过时
public void complete() {
// ...订单完成逻辑
setChanged();
notifyObservers(this); // 发送通知
}
}
问题诊断: 1) `Observable` 是一个类,限制了继承灵活性;2) 没有提供取消注册的明确生命周期管理,容易导致观察者对象无法被GC回收(内存泄漏);3) 通知是同步的,一个观察者的阻塞会拖慢整个通知流程。
重构方案: 使用 `java.beans.PropertyChangeSupport` 或更现代化的事件发布/订阅机制,特别是Spring的 `ApplicationEvent`。
// 方案:使用Spring事件机制
// 1. 定义事件
public class OrderCompletedEvent extends ApplicationEvent {
private final Order order;
public OrderCompletedEvent(Object source, Order order) {
super(source);
this.order = order;
}
public Order getOrder() { return order; }
}
// 2. 发布事件(在Service中)
@Service
public class OrderService {
@Autowired
private ApplicationEventPublisher eventPublisher;
@Transactional
public void completeOrder(Long orderId) {
Order order = // ... 完成订单的业务逻辑
eventPublisher.publishEvent(new OrderCompletedEvent(this, order));
}
}
// 3. 监听事件(异步执行,避免阻塞)
@Component
public class EmailNotificationListener {
@Async // 异步处理
@EventListener
public void handleOrderCompleted(OrderCompletedEvent event) {
// 发送邮件...
}
}
踩坑提示: 务必考虑事件的处理顺序和失败补偿。对于关键业务,异步监听可能需要结合事务事件监听器或消息队列来保证可靠性。
总结与核心原则
回顾这些误用,其根源往往在于:为了使用模式而使用模式,却忽略了模式背后的设计原则。在代码审查中,我通常会带着这几个问题去审视设计模式的使用:
- 是否真正解决了问题? 还是仅仅增加了复杂度?
- 是否遵循了SOLID原则? 特别是单一职责和开闭原则。
- 生命周期和依赖管理是否清晰? 尤其是单例和观察者。
- 是否便于测试? 能够轻松地Mock或替换其中的组件。
设计模式是“术”,设计原则才是“道”。在代码审查中,我们的目标不是写出最“模式化”的代码,而是最清晰、灵活、可维护的代码。下次当你准备引入一个模式时,不妨先停下来想想,是否有更简单直接的方式?如果必须用,是否用对了地方?希望这些实战中的反思和重构思路,能帮助你在代码审查中更有火眼金睛。

评论(0)