29、设计模式与反模式:常见的设计模式误用案例
设计模式这东西,用好了是神器,用砸了就是灾难。我见过太多团队,学了几个模式就到处乱套,结果代码比不用模式还烂。今天咱们就聊聊那些常见的误用案例——说白了,就是告诉你哪些坑我踩过,你别再踩了。
反模式一:滥用单例,测试成噩梦
单例模式,可能是被误解最深的模式。很多人觉得「全局只有一个实例」很酷,于是日志、配置、数据库连接、缓存……全搞成单例。结果呢?
我曾经接手过一个支付系统,里面有个 PaymentProcessor 类,被设计成单例。你猜怎么着?每次跑测试,都得先清空 JVM 里的单例缓存,否则上一个测试的 mock 对象会污染下一个测试。那段时间,我们团队光写测试清理代码就花了两天。
// ❌ 反模式:滥用单例
public class PaymentProcessor {
private static PaymentProcessor instance;
private PaymentGateway gateway;
private PaymentProcessor() {
// 硬编码依赖,无法替换
this.gateway = new AlipayGateway();
}
public static synchronized PaymentProcessor getInstance() {
if (instance == null) {
instance = new PaymentProcessor();
}
return instance;
}
public void process(Order order) {
gateway.charge(order.getAmount());
}
}
// 测试时:想 mock gateway?没门!
// 除非改源码,否则只能连真实支付宝测试
为什么会这样?因为单例本质上是个全局状态。全局状态和单元测试天生八字不合。你想想看,测试要求每个用例独立、可重复,而全局状态让所有测试共享同一个实例,互相干扰。
// ✅ 正确做法:依赖注入 + 工厂模式
public class PaymentProcessor {
private final PaymentGateway gateway;
// 通过构造器注入,测试时可以传 mock
public PaymentProcessor(PaymentGateway gateway) {
this.gateway = gateway;
}
public void process(Order order) {
gateway.charge(order.getAmount());
}
}
// 测试代码
@Test
void testPayment() {
PaymentGateway mockGateway = mock(PaymentGateway.class);
PaymentProcessor processor = new PaymentProcessor(mockGateway);
// 现在可以随意验证行为
}
反模式二:工厂泛滥,类爆炸
工厂模式本身没问题,但有些人恨不得给每个对象都配个工厂。我见过一个项目,总共 50 个业务类,却搞了 40 个工厂类。代码结构变成了「工厂的工厂的工厂」——三层抽象,最后连作者自己都找不到具体实现在哪。
// ❌ 反模式:为每个产品建一个工厂
public class NormalOrderFactory {
public Order createOrder() { return new NormalOrder(); }
}
public class GroupOrderFactory {
public Order createOrder() { return new GroupOrder(); }
}
public class FlashOrderFactory {
public Order createOrder() { return new FlashOrder(); }
}
// ... 每新增一种订单,就多一个工厂类
说白了,工厂模式的核心目的是「封装创建逻辑」,不是「每个类都要有个工厂」。如果创建逻辑很简单(就是 new 一下),你搞个工厂纯属脱裤子放屁。
// ✅ 正确做法:用简单工厂 + 注册机制
public class OrderFactory {
private static final Map<String, Supplier<Order>> registry = new HashMap<>();
static {
registry.put("normal", NormalOrder::new);
registry.put("group", GroupOrder::new);
registry.put("flash", FlashOrder::new);
}
public static Order createOrder(String type) {
Supplier<Order> supplier = registry.get(type);
if (supplier == null) {
throw new IllegalArgumentException("Unknown order type: " + type);
}
return supplier.get();
}
}
// 新增订单类型时,只需注册,无需新建工厂类
registry.put("premium", PremiumOrder::new);
反模式三:策略模式变成 if-else 豪华版
策略模式的本意是「把算法抽出来,让它们可以互相替换」。但很多人用着用着,就变成了「用策略模式包装 if-else」——每个策略类里就一行代码,然后客户端写个长长的 switch 来选策略。
// ❌ 反模式:策略类过于细粒度
public interface DiscountStrategy {
double calculate(double price);
}
public class NoDiscount implements DiscountStrategy {
public double calculate(double price) { return price; }
}
public class TenPercentDiscount implements DiscountStrategy {
public double calculate(double price) { return price * 0.9; }
}
public class TwentyPercentDiscount implements DiscountStrategy {
public double calculate(double price) { return price * 0.8; }
}
// ... 每个折扣率一个类,共 20 个
// 客户端:还是得写 switch
switch (discountType) {
case "NONE": strategy = new NoDiscount(); break;
case "10%": strategy = new TenPercentDiscount(); break;
// ...
}
你想想看,这跟直接写 if-else 有什么区别?代码量反而更多了。策略模式应该用在「算法逻辑复杂」或「运行时需要动态切换」的场景。如果只是参数不同,用函数式接口或枚举就够了。
// ✅ 正确做法:用枚举 + 函数式接口
public enum DiscountType {
NONE(price -> price),
TEN_PERCENT(price -> price * 0.9),
TWENTY_PERCENT(price -> price * 0.8);
private final UnaryOperator<Double> calculator;
DiscountType(UnaryOperator<Double> calculator) {
this.calculator = calculator;
}
public double apply(double price) {
return calculator.apply(price);
}
}
// 使用:直接传枚举,无需创建策略对象
double finalPrice = DiscountType.TEN_PERCENT.apply(100.0);
反模式四:观察者模式导致内存泄漏
观察者模式在事件驱动系统里很常见,但有个经典问题:观察者注册了却不注销。我调试过一个线上故障,用户打开页面后,每次操作都会触发一个事件,而事件监听器没有被正确移除。结果呢?内存里的观察者列表越积越多,最后 OOM 了。
// ❌ 反模式:只注册不注销
public class EventBus {
private List<Listener> listeners = new ArrayList<>();
public void register(Listener listener) {
listeners.add(listener); // 从不检查重复,也不提供移除方法
}
public void fire(Event event) {
listeners.forEach(l -> l.onEvent(event));
}
}
// 客户端:每次打开页面都注册,关闭页面不注销
// 结果:listeners 无限增长
// ✅ 正确做法:使用弱引用或显式注销
public class SafeEventBus {
private List<WeakReference<Listener>> listeners = new ArrayList<>();
public void register(Listener listener) {
listeners.add(new WeakReference<>(listener));
}
public void fire(Event event) {
listeners.removeIf(ref -> ref.get() == null); // 清理已回收的引用
listeners.forEach(ref -> {
Listener l = ref.get();
if (l != null) l.onEvent(event);
});
}
}
知识体系总览
下面这张图,是我总结的设计模式误用全景。你可以对照着检查自己的项目:
总结:怎么避免掉进反模式?
我个人的经验是三条:
- 先有坏味道,再谈模式。 别为了用模式而用模式。代码出现重复、难以测试、难以扩展时,才考虑用模式解决。
- 模式是手段,不是目的。 如果用了模式后代码更复杂了,那一定是哪里搞错了。好的模式应用应该让代码更简单。
- 多写测试。 测试是反模式的照妖镜。单例好不好测?工厂有没有必要?跑一遍测试就知道了。
我记得有一次 code review,看到同事给一个只有两个字段的 DTO 写了工厂模式。我问他为什么,他说「为了以后扩展」。我说:「那你现在扩展了吗?」他说没有。我说:「那就先别加。等真正需要的时候,再重构也来得及。」——这就是 YAGNI 原则(你不会需要它)。
设计模式本身没有错,错的是滥用。保持简单,保持可测试,比套用任何模式都重要。
公众号:蓝海资料掘金营,微信deep3321