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-elseswitch,等真的出现重复创建逻辑时,再重构出工厂。别提前设计,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);  // 必须注销!
    }
}

四、知识体系总览

下面这张图总结了本章的核心逻辑,帮你一眼看清三个反模式的成因、表现和解决方案。

设计模式反模式与陷阱 单例模式 滥用导致全局状态 成因 · 全局访问的便利性诱惑 · 缺乏作用域意识 表现 · 可变状态被多处修改 · 线程安全问题 · 测试困难 解决方案 · 无状态单例 · 依赖注入 · 作用域限定 工厂模式 过度工程化 成因 · 为未来过度设计 · 盲目追求模式 表现 · 工厂类比产品类多 · 抽象层次过多 · 代码难以理解 解决方案 · 按需重构 · YAGNI 原则 · 简单工厂优先 观察者模式 内存泄漏 成因 · 忘记注销观察者 · 强引用持有 表现 · 内存占用持续增长 · 页面无法回收 · OOM 风险 解决方案 · 弱引用 · 生命周期绑定 · 显式注销

五、总结与避坑指南

好了,咱们把这三个坑串起来看:

  • 单例:别让它持有可变状态,否则就是全局毒瘤。
  • 工厂:别为了「可能需要的扩展」而过度设计,先写简单的。
  • 观察者:注册了就要注销,这是铁律。
我曾经犯过一个错:在一个微服务项目里,把配置中心做成了单例,结果每个服务启动时都去修改同一个配置对象,导致服务间互相影响。后来改成每个服务独立加载配置,问题才解决。所以啊,单例的作用域一定要想清楚

设计模式是工具,不是目的。用的时候多问自己一句:这个模式真的解决了当前的问题吗?还是只是为了用模式而用模式?

嗯,今天就聊到这儿。记住这些坑,写代码的时候多留个心眼,能省不少 debug 的时间。


公众号:蓝海资料掘金营,微信deep3321