PHP怎样实现备忘录设计模式_PHP实现备忘录设计模式方法【架构】
PHP备忘录模式:避开序列化陷阱,实现轻量级状态回滚

免费影视、动漫、音乐、游戏、小说资源长期稳定更新! 👉 点此立即查看 👈
说到在PHP里实现备忘录模式,一个常见的误区是立刻想到要创建独立的Memento类。但实际情况是,绝大多数场景下,你根本不需要一个单独的Memento类。更简洁、更安全的做法,是直接在业务对象内部添加sa veState()和restoreState()方法,用数组来保存关键属性。为什么?因为真实项目里90%的“回滚”需求,其实只是需要恢复某几个字段的值,而不是要完整克隆或冻结整个复杂的对象关系图。生搬硬套教科书上的“发起人-备忘录-管理者”三角色结构,反而会引入不必要的耦合,隐藏序列化风险,并且对PDOStatement、Closure、DateTime这类“不可靠”成员束手无策。
绝大多数场景下不应单独写Memento类,而应在业务对象中用sa veState()和restoreState()方法以数组存关键属性;因90%回滚只需改几个字段,标准三角色结构反而增加耦合、隐藏序列化风险,且PDO、Closure、DateTime等不可靠项无法安全序列化。
为什么不要直接 serialize($this)
把整个对象序列化保存,看起来是最省事的方案,对吧?但这条路踩坑率极高,堪称“一步一雷”:
• 资源失效:PDO数据库连接、fopen()打开的文件句柄等资源类型,序列化后完全失效,反序列化时甚至会直接报错,例如Serialization of 'PDO' is not allowed。
• 闭包致命:包含Closure(匿名函数)的对象根本无法序列化,尝试serialize()会直接抛出致命错误。
• 时间陷阱:DateTime对象虽然可以序列化,但还原后其内部的时区指针可能发生错位。用var_dump()查看可能一切正常,但调用->format('c')时却会输出错误的时间。
• 循环引用:如果对象间存在父子互持等循环引用关系,serialize()可能会陷入无限递归,或者静默地截断数据。
• 私有隐患:所有private属性如果包含了资源句柄或只读逻辑,在恢复后,对象会处于一种“半损坏”状态,调用其方法很可能导致程序崩溃。
怎么安全地保存和恢复状态
核心原则其实很简单:只保存你明确知道需要回滚的字段,其他一律忽略。这样既能控制风险,又能提升性能。
• 定义保存方法:在业务类中定义一个sa veState()方法,让它返回一个纯粹的数组。这里的关键是,只挑选那些真正需要回溯状态的属性,并且处理好特殊类型。例如,把DateTime对象转换成字符串:
public function sa veState(): array
{
return [
'name' => $this->name,
'status' => $this->status,
'updated_at' => $this->updated_at instanceof DateTime ? $this->updated_at->format('Y-m-d H:i:s') : null,
];
}
• 实现恢复方法:对应的restoreState(array $state)方法,采用最直接的foreach循环赋值。这样做可以绕过可能存在的__set()魔术方法或自定义setter方法,避免在恢复状态时触发不必要的业务副作用:
public function restoreState(array $state): void
{
foreach ($state as $key => $value) {
if (property_exists($this, $key)) {
$this->$key = $value;
}
}
}
• 牢记几个要点:时间字段统一转为字符串存储;资源型属性(如$this->pdo)根本不要放进数组;而计算属性(例如getFullName()这类方法返回的值)则无需保存,恢复基础字段后它们自然会得到正确结果。
多级撤销用 SplStack 而不是数组堆栈
如果你的需求不止一步回滚,而是需要支持多次undo,那么用SplStack来存储状态快照数组,比使用普通数组更可靠。
• 值拷贝优势:SplStack进行的是值拷贝,而非引用共享。这意味着你修改当前对象的状态时,绝不会意外污染已经存入栈中的历史快照。
• 操作安全:它不依赖array_push()和array_pop()的手动索引管理,避免了数组越界或顺序错乱的潜在风险。
• 使用示例:
$history = new SplStack(); $history->push($obj->sa veState()); // 保存当前状态 $obj->doSomething(); // 执行某些操作,改变状态 $obj->restoreState($history->pop()); // 恢复到最近一次保存的状态
• 重要提醒:记住,只向SplStack里推送数组、字符串、数字、null这类纯数据。千万不要把DateTime实例或任何对象直接push进去,否则又回到了序列化的老问题上。
哪些情况才值得写完整 Memento 类
那么,独立备忘录类就完全无用武之地了吗?也不是。但启用它需要满足相当严格的条件,可以说是“不得已而为之”的选择:
• 强一致性要求:对象内部存在强不变性约束。例如,一个账户对象的“金额”和“状态”必须同步变更,分开存储会破坏业务一致性,必须作为一个整体快照保存。
• 跨生命周期存续:需要在多个方法调用之间,甚至跨请求生命周期保持完整的中间状态。比如一个复杂的分步表单提交流程,每一步都可能需要回退到上一步的完整状态。
• 快照即对象:业务逻辑明确要求“快照本身就是一个完整的、可传递的值对象”,并且这个对象本身不持有任何外部依赖(没有PDO、没有文件句柄、没有闭包)。
• 团队规范保障:团队已经建立了严格的编码规范,确保所有Memento类的构造函数只接收经过校验的数组(白名单模式),绝不允许直接传入原始业务对象,从源头杜绝污染。
即便以上条件都满足,也强烈建议将Memento类标记为final,禁止继承。这是为了防止子类通过扩展偷偷引入不可序列化的属性,让好不容易建立起来的防线功亏一篑。
游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。
同类文章
怎么利用 System.err 输出错误流并在控制台中以醒目的颜色标记(取决于终端)
怎么利用 System err 输出错误流并在控制台中以醒目的颜色标记(取决于终端) System err 默认行为不带颜色,终端是否显示颜色取决于自身支持 首先得明确一点:System err 本质上只是 Ja va 标准库里的一个 PrintStream 对象。它本身并不负责“颜色”这种花哨的玩
如何在 Java 中使用 ThreadLocal.remove() 确保在线程池复用场景下不会发生数据污染
如何在 Ja va 中使用 ThreadLocal remove() 确保在线程池复用场景下不会发生数据污染 说到线程池和 ThreadLocal 的搭配使用,一个看似不起眼、实则极易“踩坑”的细节就是数据清理。想象一下,你精心设计的线程池正在高效运转,却因为某个任务留下的“数据尾巴”,导致后续任务
怎么利用 Arrays.asList() 转换出的“受限列表”理解其对 add() 等修改操作的限制
Arrays asList():一个“受限”但实用的列表视图 在Ja va开发中,Arrays asList()是一个高频使用的方法,但你是否真正了解它返回的是什么?一个常见的误解是,它直接生成了一个标准的ArrayList。事实并非如此。 简单来说,Arrays asList()返回的并非我们熟悉
如何在 Java 中利用 try-catch 实现对“软错误”的平滑感知与非侵入式监控日志记录
如何在 Ja va 中利用 try-catch 实现对“软错误”的平滑感知与非侵入式监控日志记录 在 Ja va 开发中,我们常常会遇到一些“软错误”——它们不会让程序直接崩溃,却可能悄悄影响业务的正确性或用户体验。比如,调用第三方 API 时返回了空响应、缓存查询未命中、配置文件里某个非关键项缺失
Django怎么防止Celery任务重复执行_Python结合Redis实现分布式锁
Django怎么防止Celery任务重复执行:Python结合Redis实现分布式锁 你遇到过吗?明明只发了一次任务,后台却执行了两次。这不是代码写错了,而是分布式环境下一个经典的老朋友:多个worker同时抢到了同一个活儿。 为什么Celery任务会重复执行 问题的根源在于竞争。想象一下,多个Ce
- 日榜
- 周榜
- 月榜
1
2
3
4
5
6
7
8
9
10
1
2
3
4
5
6
7
8
9
10
相关攻略
2015-03-10 11:25
2015-03-10 11:05
2021-08-04 13:30
2015-03-10 11:22
2015-03-10 12:39
2022-05-16 18:57
2025-05-23 13:43
2025-05-23 14:01
热门教程
- 游戏攻略
- 安卓教程
- 苹果教程
- 电脑教程
热门话题

