设计模式是前人经验的结晶,但“会用”和“用对”之间往往隔着一条鸿沟。在Java开发中,有5个模式几乎无处不在,却也最容易被误用。它们看似简单,实则暗藏陷阱。本文逐一拆解,帮你避开那些年我们踩过的坑。
1. 单例模式:你以为的唯一,可能只是幻觉
单例模式号称最简单,却能让无数面试官和开发者反复争论。很多人写单例时随手一写:
public class Singleton { private static Singleton instance; public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }这段代码在单线程下完美,在多线程下却会创建多个实例。于是有人加锁,有人用双重检查锁定,但稍有不慎就会写出指令重排导致的半初始化对象问题。正确姿势是使用静态内部类或枚举:
public enum Singleton { INSTANCE; public void doSomething() { } }用错的代价:全局状态失控、资源重复初始化、测试困难。更隐蔽的是,单例让依赖关系隐形,调用方无法从构造函数看出依赖,违背了“显式优于隐式”的原则。
2. 工厂模式:简单工厂不是模式,是习惯
很多人把简单工厂当作工厂模式,实际上它只是一个编程习惯。真正的工厂方法模式定义了一个创建对象的接口,让子类决定实例化哪个类。而抽象工厂则用于创建产品族。
最常见的错误是“工厂泛滥”:为每个类都建一个工厂,导致类数量爆炸。比如一个只有一种实现的接口,还要硬套工厂模式,纯粹是过度设计。
正确判断:当对象创建逻辑复杂(如需要读配置、组合多个对象)或需要灵活切换实现时,才引入工厂。否则,直接new就好——简单直接才是最好的设计。
3. 策略模式:别让策略变成“类海”
策略模式用于封装可互换的算法。但很多人一上来就为每个策略建一个类,结果十个策略十个类,加上上下文和接口,代码量翻倍。
更优雅的做法是结合Lambda表达式或枚举:
Map<String, Function<Order, BigDecimal>> strategies = new HashMap<>(); strategies.put("normal", order -> order.getPrice()); strategies.put("discount", order -> order.getPrice().multiply(new BigDecimal("0.8")));用错的代价:客户端必须理解所有策略的差异,策略类之间无法共享状态,且策略的创建逻辑被推给调用方。策略模式的核心是“选择”,而不是“实现”——别让实现细节淹没了选择逻辑。
4. 观察者模式:通知链上的隐形炸弹
观察者模式实现了发布-订阅机制,但也是最容易引发内存泄漏和性能问题的模式。常见错误是:观察者被注册后忘记注销,导致对象无法回收;或者在被观察者中直接调用观察者的方法,引发循环调用甚至死锁。
Java内置的Observable类早已被废弃,正是因为它设计上的缺陷。推荐使用PropertyChangeListener或成熟的EventBus。关键原则:通知应该是异步的、幂等的,且观察者的异常不应影响其他观察者。
用错的代价:系统变得难以调试,一个通知触发连锁反应,最终导致栈溢出或线程阻塞。
5. 建造者模式:链式调用的甜蜜陷阱
建造者模式让复杂对象的构造变得优雅,但滥用会导致代码冗长。很多人为只有两三个参数的类也写建造者,结果代码量是原来三倍。
更大的坑是:建造者模式与不可变对象结合时,如果忘记在build()中做防御性拷贝,外部仍可修改内部集合。另一个常见错误是建造者本身不是线程安全的,却被当作单例复用。
正确姿势:当参数超过4个,或存在可选参数时,才考虑建造者。同时确保build()返回的对象真正不可变。
结语
设计模式的本质是“权衡”,而非“套用”。这5个模式之所以容易用错,恰恰因为它们太常用,以至于我们忘记了思考:这里真的需要模式吗?有没有更简单的写法?记住,最好的代码不是用了多少模式,而是让后来者一眼就能看懂。模式是路标,不是枷锁——当你不再想着“我要用策略模式”,而是自然地写出可替换的算法时,才算真正掌握了它。