28、常见反模式与陷阱:滥用单例导致全局状态、过度工程化的工厂、观察者模式内存泄漏
说实话,设计模式这东西,用好了是神兵利器,用砸了就是给自己挖坑。我带过不少团队,见过太多人把模式当万能药,结果代码越写越拧巴。
今天咱们就聊聊三个最常见的坑:单例的全局状态污染、工厂模式的过度设计、以及观察者模式的内存泄漏。这些都是我亲手踩过的雷,希望能帮你绕过去。
一、单例模式:全局状态的潘多拉魔盒
单例模式本身没错,错的是滥用。我见过一个项目,几乎每个类都是单例,整个系统就是一个巨大的全局变量集合。
为什么会这样?说白了,很多开发者觉得「全局访问方便」。但你想想看,一旦某个单例持有可变状态,任何地方都能修改它,调试起来简直噩梦。
我踩过的坑
我曾经接手过一个支付系统,里面有个 PaymentConfig 单例,存着商户密钥、回调地址等信息。结果测试环境和生产环境共用同一个单例实例,测试时改了配置,生产环境也跟着变了……那天晚上我加班到凌晨三点。
正确的做法
- 无状态单例:如果一定要用单例,确保它不持有可变状态。比如工具类、日志工厂。
- 依赖注入:用 IoC 容器管理生命周期,而不是自己写单例。
- 作用域限定:比如请求级别的缓存,用 ThreadLocal 或请求上下文,别搞成全局的。
// 错误示例:可变状态单例
public class UserSession {
private static UserSession instance;
private String currentUserId; // 全局可变状态!
public static UserSession getInstance() {
if (instance == null) {
instance = new UserSession();
}
return instance;
}
public void setCurrentUserId(String id) {
this.currentUserId = id; // 任何线程都能改
}
}
// 正确示例:无状态单例
public class IdGenerator {
private static final IdGenerator INSTANCE = new IdGenerator();
private final AtomicLong counter = new AtomicLong(0);
private IdGenerator() {}
public static IdGenerator getInstance() {
return INSTANCE;
}
public long nextId() {
return counter.incrementAndGet(); // 无外部可变状态
}
}
二、工厂模式:过度工程化的陷阱
工厂模式的本意是解耦对象的创建过程。但有些人把它玩成了「为每个类都配一个工厂」——这就有点过了。
- 一个工厂只生产一种产品
- 工厂类比产品类还多
- 为了「未来扩展」而提前设计抽象工厂
我记得有个项目,团队花了三周设计了一套「万能工厂框架」,支持动态加载、热替换、配置驱动……结果上线后只用了两种产品类型。那三周的时间,说白了就是白费。
什么时候该用工厂?
| 场景 | 推荐做法 | 说明 |
|---|---|---|
| 产品类型固定且少 | 简单工厂或直接 new | 别搞抽象工厂,没必要 |
| 产品类型会动态扩展 | 工厂方法模式 | 每个产品对应一个工厂子类 |
| 产品族需要一起变化 | 抽象工厂模式 | 比如跨平台 UI 组件 |
| 创建逻辑极其复杂 | 建造者模式 | 比工厂更合适 |
if-else 或 switch,等真的出现重复创建逻辑时,再重构出工厂。别提前设计,YAGNI(You Ain't Gonna Need It)原则不是说着玩的。
三、观察者模式:内存泄漏的重灾区
观察者模式在事件驱动系统里很常见,但也是最容易出内存泄漏的地方。原因很简单:观察者注册了,但忘了注销。
你想想看,一个 Activity 或 Fragment 注册了某个事件监听,结果页面销毁了,监听器还留在被观察者的列表里。被观察者持有这个监听器的引用,导致整个页面对象无法被 GC 回收。
真实案例
我之前做一个即时通讯 App,聊天页面注册了消息接收监听器。用户频繁进出聊天室,结果内存占用一路飙升。用 MAT 一分析,发现被观察者(消息中心)里积累了上千个已经销毁的页面监听器。
解决方案
- 弱引用:用 WeakReference 包装观察者,但要注意弱引用被回收后需要清理。
- 生命周期绑定:在 Android 里用 LifecycleObserver,在 Web 里用 addEventListener 的返回值。
- 显式注销:在 onDestroy 或 dispose 方法里调用 removeObserver。
- 使用事件总线框架:比如 EventBus、RxBus,它们通常内置了生命周期管理。
// 错误示例:忘记注销
public class ChatActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MessageCenter.getInstance().addListener(new MessageListener() {
@Override
public void onMessage(Message msg) {
// 处理消息
}
});
// 没有在 onDestroy 中移除!
}
}
// 正确示例:使用弱引用 + 生命周期管理
public class ChatActivity extends Activity {
private MessageListener listener;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
listener = new MessageListener() {
@Override
public void onMessage(Message msg) {
// 处理消息
}
};
MessageCenter.getInstance().addListener(listener);
}
@Override
protected void onDestroy() {
super.onDestroy();
MessageCenter.getInstance().removeListener(listener); // 必须注销!
}
}
四、知识体系总览
下面这张图总结了本章的核心逻辑,帮你一眼看清三个反模式的成因、表现和解决方案。
五、总结与避坑指南
好了,咱们把这三个坑串起来看:
- 单例:别让它持有可变状态,否则就是全局毒瘤。
- 工厂:别为了「可能需要的扩展」而过度设计,先写简单的。
- 观察者:注册了就要注销,这是铁律。
设计模式是工具,不是目的。用的时候多问自己一句:这个模式真的解决了当前的问题吗?还是只是为了用模式而用模式?
嗯,今天就聊到这儿。记住这些坑,写代码的时候多留个心眼,能省不少 debug 的时间。