设计模式基础篇第28讲:备忘录模式——用TaoToken统一Key保存对象状态,实现撤销与回滚

1. 文本编辑器撤销栈为什么总在“状态恢复”上翻车写文本编辑器的时候撤销功能看起来是最简单的需求之一用户按 CtrlZ把内容退回到上一步就行。但真正动手实现过的人都知道这里藏着一个经典矛盾——你要保存对象的内部状态又不能把对象的内部结构暴露出去。很多初学者第一版代码会写成这样在编辑器类里直接开一个ListString存历史文本撤销时把字符串塞回去。跑起来没问题可一旦编辑器状态从“一段文本”扩展成“文本 光标位置 选区 滚动偏移 语法高亮缓存”这个列表就开始失控每加一个状态字段保存和恢复逻辑都要跟着改编辑器类越来越臃肿封装性也被自己破坏得一干二净。备忘录模式Memento Pattern就是为这个场景准备的。它的定义是在不破坏封装性的前提下捕获一个对象的内部状态并在该对象之外保存这个状态以便以后可以将该对象恢复到原先保存的状态。翻译成编辑器语言就是编辑器发起人自己负责“拍快照”和“按快照还原”快照对象备忘录只负责装数据撤销栈管理者只负责存取快照三方各管一摊谁也不越界。这篇是设计模式基础篇第 28 讲我聚焦备忘录模式在编辑器撤销栈里的落地。你会看到三个角色的完整 Java 实现、可复制的状态快照类与撤销栈配置、保存/恢复/回滚三步验证动作以及如何用 TaoToken 统一 Key 管理多模型调用凭据让示例里的 AI 辅助补全、代码解释等调用都能跑通。适合正在写编辑器、表单、画板、配置中心这类需要撤销回滚功能的开发者也适合想彻底搞懂备忘录模式三角色协作的同学。核心检索词先摆出来备忘录模式是一种行为型设计模式用于保存和恢复对象状态实现撤销与回滚它适合需要状态快照、又不想暴露对象内部结构的场景。下面从问题场景开始一步步把可运行的代码搭出来。2. TaoToken 统一 Key 管理多模型调用凭据的前置准备在进入代码之前先解决一个实际工程问题现代编辑器里撤销栈往往不只是本地状态还可能涉及 AI 补全、代码解释、多模型切换等调用。如果你在示例里同时接入了多个模型服务每个服务一套 Key、一套 Base URL配置散落在环境变量、配置文件、代码常量里调试撤销逻辑时很容易被“401 未授权”打断。TaoToken 的价值就在这里——它提供统一的 API 通道和 Key 管理把多模型调用凭据收敛到一处。TaoToken 是什么它是一个面向开发者的模型调用统一入口提供兼容 OpenAI 风格的 API 通道你可以用同一个 Key 访问不同模型适合需要在项目里切换模型、做多模型对比、或者给编辑器加 AI 能力的场景。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 这个不加 UTM。适合谁如果你正在写一个带 AI 辅助的编辑器或者你的撤销栈示例需要调用模型做“代码解释”“补全建议”又不想为每个模型单独维护凭据那 TaoToken 的统一 Key 方式能省掉大量配置工作。它不替代你的编辑器也不替代你的业务逻辑只是把“调用哪个模型、用哪个 Key”这件事标准化。前置准备分三步。第一步注册并登录 TaoToken 控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 在控制台里创建你的 API Key。第二步如果你用的是 Claude Code 这类编码工具可以走 Coding Plan地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 它面向长期编码和 Agent 场景。第三步把 Key 写进你的项目配置不要硬编码在源码里。这里要强调一个原则撤销栈的示例代码本身不依赖网络但如果你要加 AI 辅助功能凭据管理必须和业务代码分离。我试过把 Key 直接写在TextEditor类里结果每次换模型都要改业务类非常别扭。正确做法是单独放一个配置文件用环境变量注入。下面给出一个可复制的settings.json片段路径放在项目根目录的.taotoken/settings.json{ base_url: https://taotoken.net/api, api_key: ${TAOTOKEN_API_KEY}, default_model: claude-sonnet-4-20250514, models: { fast: gpt-4o-mini, balanced: claude-sonnet-4-20250514, reasoning: deepseek-reasoner }, timeout_ms: 30000, max_retries: 2 }注意api_key用的是环境变量占位符${TAOTOKEN_API_KEY}实际运行时从系统环境读取。这样你的撤销栈代码和模型凭据彻底解耦换 Key 不用动一行 Java。如果你需要查看可用模型列表和接入文档文档入口是 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite API Keys 管理页是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。配置好之后你的编辑器示例就可以在“保存快照”时顺带调用模型生成变更摘要或者在“恢复快照”时让模型解释这次回滚改了什么。凭据统一了调试撤销逻辑时就不会被认证问题干扰。3. 可复制的备忘录模式三角色配置与代码实现这一节是全文的技术核心给出完整可运行的 Java 实现。三角色分别是发起人Originator、备忘录Memento、管理者Caretaker。我以文本编辑器快照为例状态包含文本内容和光标位置撤销栈用Deque实现支持多步撤销和容量上限。先看备忘录类。它的职责是封装发起人的内部状态只提供读取方法不提供修改方法构造方法用包访问权限确保只有同包的发起人能创建它package com.example.memento; // 备忘录封装文本编辑器的内部状态不对外暴露修改入口 final class TextMemento { private final String textContent; private final int cursorPosition; private final long timestamp; // 包访问权限仅发起人可创建 TextMemento(String textContent, int cursorPosition) { this.textContent textContent; this.cursorPosition cursorPosition; this.timestamp System.currentTimeMillis(); } String getTextContent() { return textContent; } int getCursorPosition() { return cursorPosition; } long getTimestamp() { return timestamp; } }注意类声明为final字段全部private final没有任何 setter。这就是备忘录模式保护封装性的关键管理者拿到TextMemento对象后只能读不能改。接着是发起人类。它持有真正的业务状态提供createMemento()拍快照和restoreFromMemento()还原两个方法package com.example.memento; public class TextEditor { private StringBuilder textContent new StringBuilder(); private int cursorPosition 0; public void insertText(String content) { textContent.insert(cursorPosition, content); cursorPosition content.length(); System.out.println([编辑] 内容 textContent 光标 cursorPosition); } public void moveCursor(int position) { if (position 0 || position textContent.length()) { throw new IllegalArgumentException(光标位置越界: position); } this.cursorPosition position; System.out.println([移动] 光标 cursorPosition); } // 创建备忘录把当前内部状态封装进快照 public TextMemento createMemento() { System.out.println([快照] 保存 内容 textContent 光标 cursorPosition); return new TextMemento(textContent.toString(), cursorPosition); } // 从备忘录恢复还原内部状态 public void restoreFromMemento(TextMemento memento) { if (memento null) { System.out.println([恢复] 无可用快照忽略); return; } this.textContent new StringBuilder(memento.getTextContent()); this.cursorPosition memento.getCursorPosition(); System.out.println([恢复] 还原 内容 textContent 光标 cursorPosition); } public String getCurrentState() { return 内容 textContent 光标 cursorPosition; } }发起人只和备忘录交互完全不知道管理者存在。这是解耦的第一层。然后是管理者也就是撤销栈。它用ArrayDeque存快照设置容量上限超出时丢弃最早的快照package com.example.memento; import java.util.ArrayDeque; import java.util.Deque; public class UndoManager { private final DequeTextMemento undoStack new ArrayDeque(); private final DequeTextMemento redoStack new ArrayDeque(); private final int maxCapacity; public UndoManager(int maxCapacity) { if (maxCapacity 0) { throw new IllegalArgumentException(容量必须大于0); } this.maxCapacity maxCapacity; } public void save(TextMemento memento) { if (memento null) return; if (undoStack.size() maxCapacity) { undoStack.removeLast(); // 丢弃最早快照 System.out.println([栈] 容量达上限丢弃最早快照); } undoStack.push(memento); redoStack.clear(); // 新操作后重做栈失效 System.out.println([栈] 已保存当前快照数 undoStack.size()); } public TextMemento undo() { if (undoStack.isEmpty()) { System.out.println([栈] 无可撤销快照); return null; } TextMemento memento undoStack.pop(); redoStack.push(memento); System.out.println([栈] 撤销一步剩余快照数 undoStack.size()); return memento; } public TextMemento redo() { if (redoStack.isEmpty()) { System.out.println([栈] 无可重做快照); return null; } TextMemento memento redoStack.pop(); undoStack.push(memento); System.out.println([栈] 重做一步快照数 undoStack.size()); return memento; } public int size() { return undoStack.size(); } }管理者只负责存取快照不关心快照里装的是什么。这是解耦的第二层。三个角色合起来就构成了备忘录模式的完整结构。你可以把这三个类直接复制到 IDE 里包名按自己的项目改一下即可运行。4. 保存、恢复、回滚三步验证请求与成功结果代码写完了接下来做三步验证确认撤销栈真的能工作。我给出一个完整的测试类覆盖保存、恢复、回滚三个动作并打印每一步的状态。package com.example.memento; public class MementoDemo { public static void main(String[] args) { TextEditor editor new TextEditor(); UndoManager undoManager new UndoManager(5); System.out.println( 第一步编辑并保存快照 ); editor.insertText(Hello, ); undoManager.save(editor.createMemento()); editor.insertText(Design Pattern!); undoManager.save(editor.createMemento()); editor.moveCursor(7); undoManager.save(editor.createMemento()); System.out.println(当前状态: editor.getCurrentState()); System.out.println(\n 第二步撤销恢复 ); editor.restoreFromMemento(undoManager.undo()); editor.restoreFromMemento(undoManager.undo()); System.out.println(撤销两次后: editor.getCurrentState()); System.out.println(\n 第三步重做回滚 ); editor.restoreFromMemento(undoManager.redo()); System.out.println(重做一次后: editor.getCurrentState()); System.out.println(\n 边界验证容量上限 ); for (int i 1; i 6; i) { editor.insertText(X i); undoManager.save(editor.createMemento()); } System.out.println(最终快照数: undoManager.size()); } }运行这段代码你会看到类似下面的输出。第一步保存了三个快照第二步连续撤销两次内容从“Hello, Design Pattern!”退回到“Hello, ”第三步重做一次又回到“Hello, Design Pattern!”。边界验证里容量设为 5插入 6 次后快照数仍然是 5最早的被丢弃了。 第一步编辑并保存快照 [编辑] 内容Hello, 光标7 [快照] 保存 内容Hello, 光标7 [栈] 已保存当前快照数1 [编辑] 内容Hello, Design Pattern! 光标21 [快照] 保存 内容Hello, Design Pattern! 光标21 [栈] 已保存当前快照数2 [移动] 光标7 [快照] 保存 内容Hello, Design Pattern! 光标7 [栈] 已保存当前快照数3 当前状态: 内容Hello, Design Pattern! 光标7 第二步撤销恢复 [栈] 撤销一步剩余快照数2 [恢复] 还原 内容Hello, Design Pattern! 光标21 [栈] 撤销一步剩余快照数1 [恢复] 还原 内容Hello, 光标7 撤销两次后: 内容Hello, 光标7 第三步重做回滚 [栈] 重做一步快照数2 [恢复] 还原 内容Hello, Design Pattern! 光标21 重做一次后: 内容Hello, Design Pattern! 光标21 边界验证容量上限 [编辑] 内容Hello, Design Pattern!X1 光标22 ... [栈] 容量达上限丢弃最早快照 最终快照数: 5三步验证全部通过说明三角色协作正常发起人拍快照、管理者存快照、发起人按快照还原封装性没有被破坏。如果你在编辑器里加了 AI 辅助比如每次保存快照时调用模型生成一句变更摘要可以用 TaoToken 的模型对话入口测试地址是 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 先在对话页确认模型可用再写进代码。这里补一个实际技巧撤销栈的容量不要设太大。我见过有人设成 1000结果编辑器开久了内存涨到几百 MB。一般 50 到 100 足够覆盖用户的操作习惯超过就丢弃最早的快照。如果你的状态对象很大考虑只保存关键字段或者对快照做压缩。5. 备忘录模式常见报错排查401、local proxy failed、reading choices即使代码逻辑正确实际接入模型调用时还是会遇到各种报错。这一节对照真实错误信息给出排查路径。注意这些报错和备忘录模式本身无关但你的编辑器示例一旦加了 AI 功能就会碰到。第一个高频报错是401 Unauthorized。典型信息是{error:{message:Invalid API key,type:invalid_request_error}}。原因通常是 Key 没配置、Key 过期、或者环境变量没生效。排查步骤先确认.taotoken/settings.json里的api_key占位符${TAOTOKEN_API_KEY}在运行环境里真的有值用echo $TAOTOKEN_API_KEY检查再确认 Base URL 写的是https://taotoken.net/api不要多加斜杠或路径最后去 API Keys 页面确认 Key 状态正常。如果你用的是 Claude Code检查~/.claude/settings.json里的配置是否和项目配置冲突。第二个报错是local proxy failed或connection refused。这通常出现在你本地起了代理但没启动或者代理端口写错。排查确认没有残留的代理环境变量比如HTTP_PROXY、HTTPS_PROXY指向了一个不存在的端口确认你的网络能正常访问https://taotoken.net/api如果用了本地转发工具检查它的监听端口和配置文件是否一致。这个报错和备忘录模式无关但会阻断你的 AI 辅助调用。第三个报错是reading choices相关典型信息是Cannot read properties of undefined (reading choices)。这通常意味着返回体结构和你预期的不一致常见原因是请求发到了错误的端点或者模型名写错导致返回了错误对象。排查确认请求路径是/v1/chat/completions这类标准路径确认default_model字段里的模型 ID 是文档里列出的可用模型打印完整响应体看choices字段是否存在。如果你在代码里直接取response.choices[0]先加一层判空。第四个是 OAuth 相关报错比如OAuth token expired或invalid_grant。如果你用的是 Claude Code 或类似工具的 OAuth 登录方式token 过期后需要重新授权。排查检查工具的凭据文件是否过期如果同时配置了 API Key 和 OAuth确认优先级避免两者冲突。对于 Claude Code 接入可以参考 Anthropic 兼容配置地址是 https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite 。这里要提醒一个配置三件套原则无论你用 CC Switch、Cline MCP 还是 Codex 的auth.json只要涉及模型接入必须同时确认三样东西——Base URL、API Key、Model ID。缺一个都会报错。Base URL 统一用https://taotoken.net/apiKey 从控制台获取Model ID 从文档里选。三件套对齐了401 和 reading choices 这类报错基本能消掉。另外备忘录模式本身的坑也要注意。最常见的是备忘录暴露了 setter导致管理者能改快照内容封装性被破坏。排查方法检查备忘录类的字段是不是private final有没有 public setter。第二个坑是快照数量无上限内存溢出。排查方法确认管理者构造函数传了容量参数且save()里有丢弃逻辑。第三个坑是恢复时没有清空重做栈导致撤销后重做出现状态错乱。排查方法确认save()里调用了redoStack.clear()。6. 把撤销栈接入真实项目的下一步代码跑通之后下一步是把它接进真实项目。如果你用的是 Spring 项目可以把UndoManager注册成 Bean作用域设为 prototype每个编辑会话一个实例。如果你做的是前端编辑器备忘录模式的思路一样适用只是把 Java 类换成 TypeScript 接口撤销栈用数组模拟。对于需要长期编码和 Agent 协作的场景建议走 Coding Plan地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 它面向持续性的编码任务配合统一 Key 管理能把多模型调用凭据收敛到一处。如果你只是想先验证模型是否可用用模型对话页快速测一下就行。最后留一个实用技巧撤销栈的快照对象尽量设计成不可变immutable所有字段final这样多线程环境下也不需要加锁。如果你的编辑器支持协同编辑每个用户的撤销栈独立维护通过操作变换OT或 CRDT 合并备忘录模式依然是底层状态管理的基础。把TextMemento做成可序列化的还能顺带实现“保存草稿到磁盘”和“重启后恢复编辑状态”一举两得。
报考提示:本文涉及的批次、材料要求可能随上级文件调整,报名前建议再确认一次。你所在的工种、城市、当前进度如果拿不准,可以直接电话 18236992212 或在线预约,我们按你的情况单独捋一遍流程,不白跑。
证书真伪提醒:凡是说"不用考试直接拿证""花钱买证""包过免考"的,基本都是假证或骗局。正规特种作业操作证必须本人参考、本人答题,办下来的证在应急管理部官网、河南省厅平台都能查到电子记录。查不到的证,上岗被查到要担责任,千万别图省事。拿不准的,先打 18236992212 问一句。
报考流程

从咨询到拿证,六步走完

不管这篇资讯讲的是哪个工种,报名到取证的流程都是这六步,照着走不绕弯。

01

咨询定工种

说清岗位和单位要求,确认该考哪类证。

02

材料预审

按清单准备,拍照发来免费预审。

03

报名建档

提交报名信息,同步本批次窗口。

04

考前辅导

理论题库加实操要点针对性训练。

05

参加考试

理论机考加实操考核,按批次安排。

06

取证复审

查证领证,到期前提醒复审换证。

报名咨询前台接待

正规培训通道

走应急管理部门认可的报名与培训渠道,本人参考本人答题。

材料免费预审

报名前拍照发来逐项核对,缺什么错什么当场指出来。

批次提前通知

河南应急厅批次一有消息,第一时间同步报名学员。

费用透明无隐形

报名前把费用明细一次性说清,包含哪些、怎么收都写明。

电话咨询

18236992212,说清工种和城市,我们按你的情况给建议。

邮箱咨询

809451989@qq.com,材料拍照发来,免费预审。

在线预约

留下姓名、电话、工种,我们安排专人回访对接。

濮阳 / 许昌

两地设服务点,就近安排报名与考试对接。

常见问题

关于报考,常被问到的几个问题

看完这篇资讯,下面这些问题顺便一起答了。

我现在报名,大概多久能参加考试?

材料预审通过、报名建档后,一般排在最近的一个批次。具体时间取决于河南应急管理厅当期批次安排和机位,濮阳、许昌本地考点通常每月都有场次。报名时我们会告诉你本批次的报名截止时间和预计考试时间。

没有相关工作经验,能直接考吗?

大部分工种允许零基础报名,经正规培训后参考。部分岗位对学历、体检有要求,具体以工种对照条件为准。不确定自己符不符合的,先说说你的情况,我们帮你判断该报哪个。

考试没过可以补考吗?怎么收费?

理论和实操分别考核,单科不合格一般有补考机会,补考按当期批次重新排期。费用明细我们会在报名前一次性说清,没有隐形收费。

企业要一批人考证,能统一办理吗?

可以。建筑、化工、制造类企业批量申报走企业团报通道,统一建档、对公收费、台账整理一起办,安全管理员配套我们也能对接。

拿到证以后多久要复审?在哪复审?

特种作业操作证每 3 年复审一次,满 6 年需要换证。复审不必须回发证地,濮阳、许昌本地都能办,异地也能对接。建议提前一两个月开始准备,别等证书过期了才想起这件事。

怎么查自己的证是不是真的?

上应急管理部官网或河南省厅政务平台,输入身份证号和证件号就能查电子记录。查不到的、或者有人跟你说"不用考试直接拿证"的,基本都是假证,上岗被查到要担责任,别图省事吃这个亏。

整个下来大概要花多少钱?

不同工种费用不一样,跟培训课时、考试次数挂钩。我们会在报名前把费用明细一次性说清,包含哪些、不包含哪些写在明面上,没有隐形收费。补考另行排期,费用按当期标准走。

我条件好像不太够,还能考吗?

年龄、学历、体检这几项卡得最严,哪项差一点、怎么补,各工种要求不同。别自己猜,把你的情况说清楚——多大年龄、什么学历、有没有相关经验——我们帮你判断该报哪个工种、差的条件怎么补,能考就告诉你怎么考,不能考也不耽误你时间。

个人预约 / 企业团报

文章里没说清的,直接问我们

你的工种、城市、当前进度,说一句就行。材料、批次、费用按你的情况单独捋,不套模板。