设计模式(十六)迭代器模式 — 统一访问集合元素的方式,不暴露内部结构

"遍历不是问题,暴露结构才是。"

在现代软件系统中,数据结构日益复杂:嵌套 JSON、图数据库、分页 API、实时事件流......若客户端直接依赖底层存储形式(数组索引、链表指针、SQL 游标),将导致:

  • 脆弱性:一旦内部实现变更,所有调用方崩溃;
  • 安全风险:外部可篡改内部状态(如跳过校验直接修改节点);
  • 复用障碍:无法统一处理不同来源的数据(内存 vs 网络 vs 文件)。

迭代器模式(Iterator Pattern) 的本质,是通过抽象协议隔离"数据持有者"与"数据消费者" ,让遍历行为成为一种受控的服务,而非裸露的权限。

本文将从 语言规范 → 框架实现 → 架构演进 → 反模式识别 四个层次,彻底讲透这一模式的工业级应用。


🔍 第一章:重新定义迭代器 ------ 不只是 next()hasNext()

GoF 的原始定义已不足以描述现代迭代器。真正的迭代器应具备以下能力:

能力 说明 示例
顺序访问 支持线性或非线性遍历 树的中序、图的 BFS
状态隔离 多个迭代器互不干扰 同时遍历同一列表两次
延迟计算 按需生成元素(惰性求值) 无限斐波那契数列
异常安全 遍历中断后资源可释放 数据库游标自动关闭
协议兼容 遵循语言标准协议 ES6 [Symbol.iterator]

💡 关键认知升级

迭代器不是"遍历工具",而是数据访问的契约接口。它定义了"如何安全地消费数据",而非"如何存储数据"。


⚙️ 第二章:JavaScript 迭代器协议深度解析(V8 引擎视角)

ES6 的迭代器协议由两部分组成:

1. 可迭代协议(Iterable Protocol)

对象必须实现 @@iterator 方法(即 [Symbol.iterator]),返回一个迭代器。

2. 迭代器协议(Iterator Protocol)

迭代器对象必须提供 next() 方法,返回 { value, done }

仅此不够。高质量实现需考虑:

✅ 正确实现:支持多次遍历
javascript 复制代码
class Range {
    constructor(start, end) {
        this.start = start;
        this.end = end;
    }

    // 每次调用返回新迭代器(关键!)
    *[Symbol.iterator]() {
        for (let i = this.start; i <= this.end; i++) {
            yield i;
        }
    }
}

const r = new Range(1, 3);
console.log([...r]); // [1,2,3]
console.log([...r]); // [1,2,3] ← 仍有效!
❌ 错误实现:状态共享导致第二次遍历失败
javascript 复制代码
// 反模式:迭代器状态绑定到聚合对象
class BadRange {
    constructor(start, end) {
        this.current = start;
        this.end = end;
    }

    [Symbol.iterator]() {
        return this; // 返回自身 → 状态污染
    }

    next() {
        if (this.current <= this.end) {
            return { value: this.current++, done: false };
        }
        return { done: true };
    }
}

📌 最佳实践[Symbol.iterator]() 应返回全新迭代器实例,确保可重入。


☕ 第三章:Java 迭代器的工业级设计 ------ Fail-Fast 与并发控制

Java 的 Iterator 不仅是接口,更是一套并发安全策略

核心机制:modCountexpectedModCount

java 复制代码
// ArrayList.Itr 内部类片段
private class Itr implements Iterator<E> {
    int cursor;       // 下一个要返回的元素索引
    int lastRet = -1; // 上次返回的元素索引
    int expectedModCount = modCount; // 快照

    public E next() {
        checkForComodification(); // 关键检查
        // ... 返回元素
    }

    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

并发场景对比

集合类型 迭代器类型 行为 适用场景
ArrayList Fail-Fast 修改即抛异常 单线程调试
CopyOnWriteArrayList Fail-Safe(快照) 遍历副本,不受修改影响 读多写少
ConcurrentHashMap Weakly Consistent 可能看不到最新修改,但不会崩溃 高并发

🔥 架构启示:迭代器语义直接影响系统容错能力。选择集合类型前,先明确遍历一致性需求。


🌐 第四章:真实世界中的迭代器 ------ 超越内存集合

迭代器的价值在跨边界数据访问中尤为突出:

场景 1:数据库游标(Cursor)

sql 复制代码
-- PostgreSQL 游标
DECLARE user_cursor CURSOR FOR SELECT * FROM users;
FETCH 100 FROM user_cursor;
  • 客户端无需加载全表;
  • 服务端维护遍历状态;
  • 自动资源回收(超时关闭)。

场景 2:GraphQL 分页

graphql 复制代码
query {
  users(first: 10, after: "cursor_xyz") {
    edges { node { id name } }
    pageInfo { hasNextPage endCursor }
  }
}
  • endCursor 本质是迭代器状态的序列化;
  • 支持无限滚动、断点续传。

场景 3:文件流逐行读取(Node.js)

javascript 复制代码
const fs = require('fs');
const readline = require('readline');

const rl = readline.createInterface({
  input: fs.createReadStream('huge.log')
});

rl.on('line', (line) => {
  // 每行作为"next()"结果
});
  • 内存占用恒定 O(1);
  • 适用于 GB 级日志处理。

共同点 :数据生产者控制节奏,消费者被动接收,解耦时空依赖


🔄 第五章:迭代器 vs 生成器 vs 流 ------ 概念辨析

特性 迭代器(Iterator) 生成器(Generator) 流(Stream)
本质 接口协议 函数语法糖 数据处理管道
状态管理 外部对象维护 函数挂起(yield) 内部队列/背压
组合能力 需手动包装 天然支持委托(yield*) 链式操作(map/filter)
语言支持 Java/JS/Python JS/Python Java/RxJS
典型用途 集合遍历 惰性序列生成 响应式数据处理

关系图谱:

复制代码
迭代器(协议) 
  ↑ 实现方式
生成器(语法) → 可返回迭代器
  ↑ 组合增强
流(框架) → 基于迭代器构建

💡 选型建议

  • 需要自定义集合? → 实现迭代器协议;
  • 需要生成无限序列? → 用生成器;
  • 需要声明式数据转换? → 用 Stream/RxJS。

🛠️ 第六章:高性能迭代器设计 ------ 避免常见陷阱

陷阱 1:在 next() 中做重型计算

javascript 复制代码
// 反模式:每次 next 都排序
next() {
    const sorted = this.data.sort(); // O(n log n) × n → O(n² log n)
    return sorted[this.index++];
}

修复:预计算或懒初始化。

陷阱 2:忽略资源清理

java 复制代码
public class FileLineIterator implements Iterator<String> {
    private BufferedReader reader;

    public String next() { /* 读取一行 */ }

    // 忘记 close() → 文件句柄泄漏
}

修复 :实现 AutoCloseable,或使用 try-with-resources。

陷阱 3:并发修改未同步

java 复制代码
// 多线程调用 next() 可能重复或跳过元素
public E next() {
    return items[index++]; // 非原子操作
}

修复:加锁或使用线程安全集合。


🧪 第七章:测试迭代器的正确性 ------ 边界用例清单

高质量迭代器必须通过以下测试:

java 复制代码
@Test
void testIteratorContract() {
    MyCollection<Integer> coll = new MyCollection<>(1, 2, 3);
    Iterator<Integer> it = coll.iterator();

    // 1. 初始状态
    assertTrue(it.hasNext());

    // 2. 顺序正确
    assertEquals(1, it.next());
    assertEquals(2, it.next());
    assertEquals(3, it.next());

    // 3. 结束后不可再 next()
    assertFalse(it.hasNext());
    assertThrows(NoSuchElementException.class, it::next);

    // 4. 可重入(新迭代器)
    Iterator<Integer> it2 = coll.iterator();
    assertEquals(1, it2.next());

    // 5. 并发安全(若承诺)
    // ... 多线程测试
}

📌 黄金法则:迭代器的行为应像"只读光标",不可变、可预测、可重放。


🤖 第八章:前端框架中的迭代器思维 ------ React Suspense 与流式 SSR

React 18 的 流式服务端渲染(Streaming SSR) 本质是迭代器思想的延伸:

jsx 复制代码
// App.jsx
<Suspense fallback="Loading...">
  <Comments />
</Suspense>
  • 服务端按组件边界"yield" HTML 片段;
  • 客户端逐步 hydrate;
  • 用户无需等待整页加载。

这相当于:

  • 聚合对象:React 应用树;
  • 迭代器:ReactDOMServer.renderToReadableStream();
  • next():每个 Suspense boundary 的输出。

架构升华:迭代器模式从"数据遍历"扩展到"UI 渲染流程控制"。


⚖️ 第九章:何时不该用迭代器?

迭代器并非万能。以下场景应避免:

场景 原因 替代方案
需要随机访问 迭代器只能顺序前进 直接索引(数组)
频繁中间插入/删除 迭代器可能失效 使用支持游标的数据库
性能极度敏感 方法调用开销 手写 for 循环(如游戏引擎)
数据量极小(<10) 过度设计 直接遍历

🎯 决策树

数据是否复杂?→ 是 → 是否需统一访问?→ 是 → 用迭代器

否则,优先简单方案。


🧩 第十章:与组合模式协同 ------ 遍历树形结构

在组合模式中,迭代器可统一叶节点与容器节点的访问:

java 复制代码
interface FileSystemNode {
    String getName();
    Iterator<FileSystemNode> depthFirstIterator();
}

class Directory implements FileSystemNode {
    private List<FileSystemNode> children = new ArrayList<>();

    public Iterator<FileSystemNode> depthFirstIterator() {
        return new DFSIterator(this);
    }

    private static class DFSIterator implements Iterator<FileSystemNode> {
        private Stack<FileSystemNode> stack = new Stack<>();

        DFSIterator(Directory root) {
            stack.push(root);
        }

        public boolean hasNext() {
            return !stack.isEmpty();
        }

        public FileSystemNode next() {
            FileSystemNode current = stack.pop();
            if (current instanceof Directory) {
                // 将子节点逆序压栈(保证正序弹出)
                List<FileSystemNode> children = ((Directory) current).getChildren();
                for (int i = children.size() - 1; i >= 0; i--) {
                    stack.push(children.get(i));
                }
            }
            return current;
        }
    }
}

客户端代码完全 unaware 是否在遍历文件或目录。


🔒 第十一章:安全迭代器 ------ 防御性编程实践

对外暴露的迭代器应防止恶意使用:

1. 禁止结构修改

java 复制代码
// 返回不可变视图的迭代器
public Iterator<T> iterator() {
    return Collections.unmodifiableList(internalList).iterator();
}

2. 防御性拷贝(高安全场景)

java 复制代码
public Iterator<T> iterator() {
    return new ArrayList<>(internalList).iterator(); // 快照
}

3. 权限控制

javascript 复制代码
// 仅授权用户可遍历
[Symbol.iterator]() {
    if (!currentUser.hasPermission('READ')) {
        throw new Error('Access denied');
    }
    // ...
}

🛡️ 原则:迭代器是数据出口,必须像 API 网关一样做鉴权与限流。


📈 第十二章:性能基准测试 ------ 迭代器 vs 原生循环

在 Node.js 中测试 100 万次遍历:

方式 时间(ms) 内存(MB)
for (let i=0; ...) 12 40
for...of(原生数组) 18 40
自定义迭代器 45 42
Generator 函数 60 45

结论:

  • 原生循环最快,适合性能关键路径;
  • 迭代器开销可控(<3x),换取封装性值得;
  • 避免在热路径用 Generator

📊 建议:95% 场景用语言内置遍历,5% 自定义结构才需手写迭代器。


🧭 第十三章:未来演进 ------ 异步迭代器与 Web Streams

ES2018 引入 异步迭代器(AsyncIterator),用于处理流式异步数据:

javascript 复制代码
const fetchStream = async function* () {
    let page = 1;
    while (true) {
        const res = await fetch(`/api/data?page=\${page}`);
        const data = await res.json();
        if (data.length === 0) return;
        for (const item of data) yield item;
        page++;
    }
};

// 使用
for await (const item of fetchStream()) {
    process(item);
}

这与 Web Streams API 深度集成:

javascript 复制代码
const stream = new ReadableStream({ /* ... */ });
for await (const chunk of stream) { /* ... */ }

🌍 趋势:迭代器正从"内存遍历"走向"时空解耦的数据管道"。


📌 第十四章:终极最佳实践清单

  1. 遵循语言协议 :JS 用 [Symbol.iterator],Java 实现 Iterable
  2. 保证可重入 :每次 iterator() 返回新实例;
  3. 明确一致性模型:Fail-Fast / Fail-Safe / Weakly Consistent;
  4. 资源必须释放 :实现 close()return()(Generator);
  5. 禁止在遍历时修改结构 (除非提供安全方法如 remove());
  6. 对外暴露只读视图:防御性拷贝或不可变包装;
  7. 文档化行为:是否支持 remove?是否线程安全?
  8. 性能敏感场景回退到原生循环

🎯 结语:迭代器是数据主权的守门人

在数据爆炸的时代,谁控制了遍历,谁就控制了数据的解释权

迭代器模式远不止于"如何循环",它是一种访问控制哲学

  • 对内,隐藏实现细节,保护数据完整性;
  • 对外,提供标准化契约,促进生态协作。

从 Java 集合到 GraphQL,从数据库游标到 React Suspense,迭代器始终在幕后守护着软件世界的秩序与安全。

下次设计 API 时,请自问:
"我是在暴露数据,还是在提供服务?"

相关推荐
明洞日记36 分钟前
【设计模式手册014】解释器模式 - 语言解释的优雅实现
java·设计模式·解释器模式
未秃头的程序猿4 小时前
🚀 设计模式在复杂支付系统中的应用:策略+工厂+模板方法模式实战
后端·设计模式
雨中飘荡的记忆5 小时前
深入理解设计模式之单例模式
java·设计模式
8***29316 小时前
能懂!基于Springboot的用户增删查改(三层设计模式)
spring boot·后端·设计模式
在未来等你15 小时前
AI Agent设计模式 Day 19:Feedback-Loop模式:反馈循环与自我优化
设计模式·llm·react·ai agent·plan-and-execute
兵bing20 小时前
设计模式-访问者模式
设计模式·访问者模式
python零基础入门小白20 小时前
【万字长文】大模型应用开发:意图路由与查询重写设计模式(从入门到精通)
java·开发语言·设计模式·语言模型·架构·大模型应用开发·大模型学习
MC丶科21 小时前
Java设计模式漫画英雄宇宙-工厂模式 —Factory博士的“超级英雄制造机”!
java·设计模式·漫画
明洞日记21 小时前
【设计模式手册013】命令模式 - 请求封装的优雅之道
java·设计模式·命令模式