商城首页欢迎来到中国正版软件门户

您的位置:首页 >PHP怎样实现备忘录设计模式_PHP实现备忘录设计模式方法【架构】

PHP怎样实现备忘录设计模式_PHP实现备忘录设计模式方法【架构】

  发布于2026-05-03 阅读(0)

扫一扫,手机访问

PHP备忘录模式:避开序列化陷阱,实现轻量级状态回滚

PHP怎样实现备忘录设计模式_PHP实现备忘录设计模式方法【架构】

说到在PHP里实现备忘录模式,一个常见的误区是立刻想到要创建独立的Memento类。但实际情况是,绝大多数场景下,你根本不需要一个单独的Memento类。更简洁、更安全的做法,是直接在业务对象内部添加sa veState()restoreState()方法,用数组来保存关键属性。为什么?因为真实项目里90%的“回滚”需求,其实只是需要恢复某几个字段的值,而不是要完整克隆或冻结整个复杂的对象关系图。生搬硬套教科书上的“发起人-备忘录-管理者”三角色结构,反而会引入不必要的耦合,隐藏序列化风险,并且对PDOStatementClosureDateTime这类“不可靠”成员束手无策。

绝大多数场景下不应单独写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,禁止继承。这是为了防止子类通过扩展偷偷引入不可序列化的属性,让好不容易建立起来的防线功亏一篑。

本文转载于:https://www.php.cn/faq/2321439.html 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

热门关注