Java 工程师必会的设计模式:从策略模式到模板方法

一、为什么学设计模式

学设计模式不是为了面试背题,而是积累经过验证的解决方案

你遇到的问题,前人早就遇到过。前人把解法命名、归档,形成了设计模式。掌握这些模式,遇到问题时就能站在巨人的肩膀上,而不是从零思考。

Java JDK 本身就用了大量设计模式。读 JDK 源码时认出设计模式,能帮你更快理解它的设计逻辑。

这篇文章不会讲 23 种模式,挑最实用的 7 种,配合 JDK 里的实际用法。

二、单例模式(Singleton)

解决的问题:一个类只应该有一个实例,全局唯一。

常见写法:双重检查锁定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton {
private static volatile Singleton instance;

private Singleton() {}

public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

volatile 防止指令重排序,确保多线程安全。JDK 里的 java.lang.Runtime 就是单例 —— 每个 JVM 只有一个 Runtime 实例。

为什么不用全局变量

全局变量也能做到全局唯一,但无法防止有人 new Singleton() 创建第二个实例。单例模式把构造函数设为 private,从语言层面保证了唯一性。

三、工厂模式(Factory)

解决的问题:对象创建逻辑和使用逻辑分离,切换实现不需要改调用方代码。

简单工厂

1
2
3
4
5
6
7
8
9
10
public class NotificationFactory {
public static Notification create(String type) {
return switch (type) {
case "email" -> new EmailNotification();
case "sms" -> new SmsNotification();
case "push" -> new PushNotification();
default -> throw new IllegalArgumentException("Unknown type");
};
}
}

调用方只依赖 Notification 接口,不需要知道具体是哪种通知。

JDK 源码里的工厂

java.time.NumberFormatgetInstance()getNumberInstance() 等方法都是工厂方法。调用方不需要知道 NumberFormat 内部怎么构造 decimal format。

1
NumberFormat nf = NumberFormat.getInstance(Locale.US);

四、策略模式(Strategy)

解决的问题:一组算法可以互换,客户端代码可以根据上下文选择算法,无需修改自身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface PaymentStrategy {
void pay(double amount);
}

public class AlipayStrategy implements PaymentStrategy {
public void pay(double amount) { /* 支付宝逻辑 */ }
}

public class WechatPayStrategy implements PaymentStrategy {
public void pay(double amount) { /* 微信支付逻辑 */ }
}

public class OrderContext {
private PaymentStrategy strategy;
public void setStrategy(PaymentStrategy strategy) {
this.strategy = strategy;
}
public void checkout(double amount) {
strategy.pay(amount);
}
}

调用方:

1
2
3
OrderContext order = new OrderContext();
order.setStrategy(new AlipayStrategy());
order.checkout(100.0);

切换支付方式只需要 setStrategy(...),不需要改 OrderContext 的代码。

JDK 里的策略模式

java.util.Comparator 就是策略模式的具体应用。不同的排序规则传入不同的 Comparator 实现,集合.sort 方法不需要改动。

1
2
List<String> names = Arrays.asList("Tom", "Alice", "Bob");
names.sort((a, b) -> b.compareTo(a)); // 逆序

五、观察者模式(Observer)

解决的问题:对象状态变化时,自动通知依赖它的其他对象。

JDK 内置支持:java.util.Observable(已废弃)和 java.util.Observer(已废弃),但思想值得掌握。

手动实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface Observer {
void update(String message);
}

public class Subject {
private List<Observer> observers = new ArrayList<>();

public void addObserver(Observer observer) {
observers.add(observer);
}

public void notifyObservers(String message) {
for (Observer observer : observers) {
observer.update(message);
}
}
}

JDK 里的观察者

Spring 的 ApplicationEvent + ApplicationListener 就是观察者模式。Bean 监听 ApplicationEvent,任何地方发布事件时所有监听器都会收到通知。

1
2
3
4
5
6
@Component
public class MyListener implements ApplicationListener<MyEvent> {
public void onApplicationEvent(MyEvent event) {
// 收到通知
}
}

六、装饰器模式(Decorator)

解决的问题:动态给对象添加新功能,比继承更灵活,不会导致类膨胀。

类结构

1
2
3
InputStream
└─ FilterInputStream(装饰器基类)
└─ BufferedInputStream(具体装饰器)

JDK 的 I/O 类库是装饰器模式的经典案例:

1
2
3
4
5
6
// 基础流
FileInputStream fis = new FileInputStream("file.txt");
// 加缓冲
BufferedInputStream bis = new BufferedInputStream(fis);
// 加行号
LineNumberInputStream lis = new LineNumberInputStream(bis);

每个装饰器都包装了上一个流,层层叠加。运行时动态组合功能,比写一个继承链清晰得多。

装饰器 vs 继承

继承在编译时确定功能,装饰器在运行时动态组合。如果用继承:

1
FileInputStream → BufferedFileInputStream → LineNumberBufferedFileInputStream

组合一多,类就爆炸了。装饰器模式解决了这个问题。

七、迭代器模式(Iterator)

解决的问题:统一遍历接口,客户端代码不需要知道集合内部结构。

JDK 集合框架全部实现了 java.util.Iterator

1
2
3
4
5
6
7
8
9
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");

Iterator<String> it = list.iterator();
while (it.hasNext()) {
String item = it.next();
System.out.println(item);
}

调用方不需要知道 ArrayList 底层是数组还是链表,遍历方式统一。

for-each 的背后

1
2
3
for (String item : list) {
System.out.println(item);
}

语法糖,编译器把它变成:

1
2
3
4
5
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String item = it.next();
// ...
}

八、模板方法模式(Template Method)

解决问题:算法骨架固定,部分步骤由子类实现,复用共同逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public abstract class DataProcessor {
// 模板方法,final 防止子类修改骨架
public final void process() {
connect();
processData();
disconnect();
}

protected abstract void processData();

private void connect() { /* 连接逻辑 */ }
private void disconnect() { /* 断开逻辑 */ }
}

public class XmlProcessor extends DataProcessor {
protected void processData() {
// 处理 XML
}
}

public class JsonProcessor extends DataProcessor {
protected void processData() {
// 处理 JSON
}
}

connect()disconnect() 是固定逻辑,子类不需要关心。processData() 由子类实现,提供各自的数据处理逻辑。

JDK 里的模板方法

java.io.InputStreamread() 方法就是模板方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public int read(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if (off < 0 || off > b.length || len < 0) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
// 这里调用了抽象方法 read()
int c = read();
if (c == -1) {
return -1;
}
b[off] = (byte)(c & 0xFF);
// ...
}

read(byte[], int, int) 是骨架,调用无参数的 read() 由子类实现。

九、总结

模式 解决问题 JDK 里的例子
单例 全局唯一实例 java.lang.Runtime
工厂 创建和使用分离 NumberFormat.getInstance()
策略 算法可切换 Comparator
观察者 状态变化自动通知 Spring ApplicationListener
装饰器 动态添加功能 BufferedInputStream
迭代器 统一遍历接口 Iterator
模板方法 算法骨架复用 InputStream.read()

设计模式不是银弹。它的价值在于让代码结构变得可预测 —— 当你认出某个模式时,你就能预判代码接下来会怎么走。这对读源码、做 Code Review、还有和团队沟通,都很有帮助。