主题
状态机与其他实用模式:状态、迭代器、享元、组合
本文是设计模式系统学习系列的 L2 核心篇。前置:[9. 适配器、外观与桥接]。 学完可以配合面试题食用:并发容器全景
为什么需要状态机
订单状态流转是每个业务系统都会遇到的问题。刚创建 -> 已支付 -> 已发货 -> 已完成 -> 已取消,中间还有退款中、退货中。如果用一个 state 字段加一堆 if-else 来判断"当前状态能不能做某事",代码很快就会变成一团乱麻。
java
// 反例:if-else 蠕变
if ("WAIT_PAY".equals(order.getState()) && "pay".equals(action)) {
// 支付逻辑
order.setState("PAID");
} else if ("PAID".equals(order.getState()) && "pay".equals(action)) {
throw new RuntimeException("已支付,不能重复支付");
} else if ("PAID".equals(order.getState()) && "ship".equals(action)) {
// 发货逻辑
order.setState("SHIPPED");
}
// 每加一个状态或动作,这个类就膨胀一次这个写法的问题:状态转移逻辑分散在条件分支里,加一个状态就要改所有涉及的地方,违反开闭原则。状态模式就是解决这个问题的。
状态模式:把状态转移收拢到一张表里
状态模式的思路很简单:把每个状态的行为和转移规则封装成一个对象,用一个统一的转移表或枚举来管理所有合法路径。
java
public enum OrderState {
WAIT_PAY {
@Override
public OrderState pay() {
System.out.println("支付成功");
return PAID;
}
@Override
public OrderState cancel() {
System.out.println("未支付订单已取消");
return CANCELED;
}
// 其他动作:不支持则抛异常
@Override
public OrderState ship() {
throw new IllegalStateException("未支付不能发货");
}
},
PAID {
@Override
public OrderState ship() {
System.out.println("发货成功");
return SHIPPED;
}
@Override
public OrderState refund() {
System.out.println("进入退款流程");
return REFUNDING;
}
@Override
public OrderState pay() {
throw new IllegalStateException("已支付,不能重复支付");
}
},
SHIPPED {
@Override
public OrderState confirm() {
System.out.println("确认收货");
return COMPLETED;
}
@Override
public OrderState refund() {
System.out.println("进入退货退款流程");
return RETURNING;
}
},
COMPLETED, CANCELED, REFUNDING, RETURNING;
// 默认实现:不支持的动作抛异常
public OrderState pay() { throw new IllegalStateException("不支持"); }
public OrderState cancel() { throw new IllegalStateException("不支持"); }
public OrderState ship() { throw new IllegalStateException("不支持"); }
public OrderState confirm() { throw new IllegalStateException("不支持"); }
public OrderState refund() { throw new IllegalStateException("不支持"); }
// 转移表验证(可选,双重保障)
public static boolean isValidTransition(OrderState from, OrderState to) {
try {
// 简单内部验证逻辑
return true;
} catch (Exception e) {
return false;
}
}
}状态 vs 策略:结构上几乎一样,但语义不同。策略模式里各个策略互相不知道对方存在,调用方决定用哪个;状态模式里状态之间会互相转移(PAID 调 ship() 变成 SHIPPED),状态对象自己决定下一个状态是谁。
工程实现补充:小规模状态机用枚举+转移表就够了。复杂场景(十几种状态、带条件分支、超时自动转移)上 Spring StateMachine 或 COLA StateMachine,它们提供了 DSL 来声明式定义转移规则。
迭代器:遍历集合的统一接口
迭代器模式是 Java 里最普及的模式之一——Collection 接口继承了 Iterable,所以所有集合类都能用 for-each 遍历。
java
// for-each 的本质
List<String> list = Arrays.asList("A", "B", "C");
for (String s : list) { // 编译器转成
System.out.println(s); // Iterator<String> it = list.iterator();
} // while(it.hasNext()) { s = it.next(); }fail-fast 机制:遍历过程中如果集合被结构性修改(add/remove),迭代器会抛 ConcurrentModificationException。实现方式是每个集合维护一个 modCount 计数器,迭代器初始化时记住这个值,每次 next() 检查是否一致。
java
// 触发 fail-fast 的典型场景
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
for (String s : list) {
if ("B".equals(s)) {
list.remove(s); // ❌ 触发 ConcurrentModificationException
}
}
// 正确做法:用 Iterator.remove() 或 Collectors.toList() 收集要删的注意:CopyOnWriteArrayList 没有 fail-fast,因为它遍历的是快照,写时复制,不会和读冲突。
享元:对象复用,减少内存开销
享元模式的核心是"共享"——相同的数据只存一份,不同对象持有引用。Java 里最典型的例子:
Integer缓存:默认缓存 -128~127 的 Integer 对象String常量池:相同字符串字面量指向同一对象- 连接池、线程池:复用昂贵的创建成本
java
// Integer 缓存实证
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true,因为命中缓存
Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false,超出缓存范围,各自 new 对象
// 所以 Integer 比较永远用 .equals(),不要用 ==享元模式的关键区分:内部状态(共享,不变)和外部状态(由上下文传入,可变)。比如 String 的字符数组是内部状态,拼接/截取产生的不同引用是外部状态。Integer 缓存池里相同的数值是内部状态,不同引用变量是外部状态。
组合模式:树形结构的统一处理
组合模式让客户端可以一致对待单个对象和组合对象。典型场景:菜单树、组织架构、文件系统。
java
// 组件接口
public interface FileSystemNode {
String getName();
long getSize();
void print(int indent);
}
// 叶子节点:文件
public class FileNode implements FileSystemNode {
private String name;
private long size;
public FileNode(String name, long size) {
this.name = name;
this.size = size;
}
@Override public String getName() { return name; }
@Override public long getSize() { return size; }
@Override
public void print(int indent) {
System.out.println(" ".repeat(indent) + name + " (" + size + "B)");
}
}
// 组合节点:目录
public class DirectoryNode implements FileSystemNode {
private String name;
private List<FileSystemNode> children = new ArrayList<>();
public DirectoryNode(String name) { this.name = name; }
public void add(FileSystemNode child) { children.add(child); }
@Override public String getName() { return name; }
@Override
public long getSize() {
return children.stream().mapToLong(FileSystemNode::getSize).sum();
}
@Override
public void print(int indent) {
System.out.println(" ".repeat(indent) + name + "/");
for (FileSystemNode child : children) {
child.print(indent + 1);
}
}
}组合模式常和迭代器模式一起用——递归遍历目录树本身就是迭代器思想的应用。
常见误区与小结
- 状态模式过度设计:只有 3 个状态、2 种动作的简单状态机,用 if-else 反而更清晰。状态模式的成本在"每个状态一个类"的维护,不值得为小规模状态机引入。
- 迭代器中的
remove()是可选操作:ArrayList的迭代器支持remove(),但不是所有集合的迭代器都支持(比如UnmodifiableList抛异常)。调用前检查文档或remove()的抛异常说明。 - 享元池的大小不是越大越好:Integer 缓存上限调高会占用更多内存,除非通过 JVM 参数
-XX:AutoBoxCacheMax显式配置,否则默认只到 127。 - 组合模式递归陷阱:目录里出现循环引用(软链接指向父目录)会导致
getSize()死循环。实现时需要加 visited 集合做环路检测,或者限制只遍历一层。
小结:状态模式把状态转移从 if-else 中解放出来,迭代器解决遍历的通用接口问题,享元做对象复用减少内存,组合模式处理树形结构。这四种模式在实际工程中出现的频率很高——状态机做订单/审批流,迭代器是集合基操,享元体现在各种池化技术里,组合模式在菜单/组织/文件系统里随处可见。下一篇会把这些模式放到 Spring、MyBatis 和 JDK 源码里看它们怎么实际落地。
参考
《设计模式:可复用面向对象软件的基础》(GoF 书)第 5 章行为模式(State)、第 4 章结构模式(Composite、Flyweight) JDK 源码:
java.util.Iterator、java.lang.Integer.IntegerCache、java.util.ArrayList.Itr的 modCount 检查 Spring StateMachine 官方文档:https://spring.io/projects/spring-statemachine