Web 开发中的设计模式 - #2 Memento 简介 Memento 请少一些对话,多一些行动 设计模式 结语

2025-06-07

Web 开发中的设计模式 - #2 Memento

介绍

纪念

请少说话,多行动

设计模式

最后的话

点击此处查看更新版本

介绍

你们可能还记得,在试播集里,我说过要用三个例子来解释Command :一个 UI Kit、一个 CQRS 应用和一个 Electron 中的撤销/重做实现。但在Command 集中,我没有提供后者,原因很简单:我是个混蛋。

此外,对我来说,用这个例子来解释属于四人帮经典模式的另一种行为模式1Memento更有意义

纪念

等等等等。请输入代码

例如:计算器

假设你正在使用计算器。你提供一个表达式,它会为你进行计算。为了简单起见,我们只考虑它的一个方法power

这个计算器背后的逻辑位于一个名为的类中,Calculator它看起来应该是这样的:

class Calculator {
    // State
    private string display;
    // and a whole lot of unrelated other fields

    // Resolves expressions like x^y
    private power(string expression): number;

    // Writes on display
    setState(string display): void;

    // Parse what's on the display, calculates and overrides the display
    calculate(): number;
}
Enter fullscreen mode Exit fullscreen mode

有一天,我们决定为这个应用程序实现一个撤销机制。实现这个机制的第一个想法可能是简单地应用刚才操作的逆函数。

快速数学

不幸的是,这对于该功能不起作用power

例如:撤消y = power(x, 2)将应用sqrt(y, 2),但power(2, 2)和会power(-2, 2)产生相同的结果,因此您不能x仅通过使用就明确地获得y

此时,当您calculate和时将先前的状态保存在快照中undo,然后使用这样的快照重置计算器的状态看起来更简单,更有效。

Memento提供了一种巧妙的方法来解决这个问题。

这是关于什么的?

意图
在不违反封装的情况下,捕获并外部化对象的内部
状态,以便对象稍后可以恢复到该状态。

是的,您刚刚赢得了这一轮“猜引言”:这句话来自“四人帮”

这里的想法非常简单:我们希望有一种系统的方法来存储给定对象的内部状态的快照,而不暴露这种状态,以便以后能够恢复。

《心灵捕手》

如果你还在疑惑为什么不应该暴露状态,或许你仍然没有像应该的那样担心耦合。这确实很糟糕。不过,你仍然可以通过阅读这篇文章来解决这个问题。我会在这里等你。

...

完成了吗?我们可以开始实践一下Memento 了

实践中的模式

99个问题

首先要说的是:为什么这个模式叫做 Memento?Memento是一个拉丁词,可以安全地翻译成“提醒者2”Calculator 。这是我们用来存储我们感兴趣的部分状态的对象。

Calculator,即状态的起源,被称为Originator,而这个故事的第三个角色将负责使整个事情正常运转,被称为CareTaker

总而言之,Memento 中的参与者及其职责如下:

  • 发起人
    • 创建一个 Memento 来存储内部状态;
    • 使用 Mementos 恢复其状态;
  • 纪念品
    • 存储 Originator 内部状态的不可变快照;
    • 可由发起人访问;
  • 看护人
    • 存储纪念品;
    • 从不操作或读取 Mementos;

实际上,这些将会变成类似这样:

// Originator
class Calculator {
    private string display;

    private power(string expression): number;

    setState(string display): void;
    calculate(): number;
    save(): Snapshot;
    restore(Snapshot snapshot): void; 
}

// Memento
class Snapshot {
    private string state;

    getState(): state;
}

// CareTaker
class Application {
    Calculator calculator;
    Array<Snapshot> undoSnapshots;
    Array<Snapshot> redoSnapshots;

    calculate(): void {
        const snapshot = this.calculator.save()
        this.undoSnapshots.push(snapshot)
        this.redoSnapshots = []
        this.calculator.calculate()
    }

    undo(): void {
        const snapshot = this.undoSnapshots.pop()
        this.redoSnapshots.push(snapshot)
        this.calculator.restore(snapshot)
    }

    redo(): void {
        const snapshot = this.redoSnapshots.pop()
        this.undoSnapshots.push(snapshot)
        this.calculator.restore(snapshot)
    }
}
Enter fullscreen mode Exit fullscreen mode

太棒了!我明天怎么用这个?

有了Memento,我们算是幸运的:你不需要寻找极其复杂的用例来实现它。撤消/重做场景是该模式迄今为止最常发挥作用的地方,但每次你需要将对象恢复到上一个​​阶段时,它都可以轻松地重复使用。

您需要另一个例子,不是吗?

假设你有一个 Web 应用的个人资料页面。用户点击了“编辑个人资料”,但在执行了一些操作后,他们“取消”了操作。除非你每次都想通过 AJAX 调用重新获取用户信息,否则一个好主意是存储一个备忘录,其中包含用户个人资料的快照,以便在取消操作后恢复。

Memento是实现这一目标的唯一方法吗?不是。在这种情况下,另一个相当常见的模式是Prototype,它可能是下一集的主题。也可能不是,谁知道呢?无论如何,你现在需要了解的是, Prototype提供了另一种创建对象状态副本的方法,只不过方式不同。

总而言之,当您必须回顾对象的历史时,拍摄快照可以让您的生活变得更轻松。

快照

你的下一个问题可能是:这仅仅是方便还是有必要?我们在计算器的例子中看到,有时反转上一个操作并不足以恢复到之前的状态。不幸的是,这不仅适用于不可逆的数学函数,而且适用于任何方法产生副作用的情况。在这种情况下,通常情况下,创建快照是安全地恢复到之前状态的唯一方法。

那么,问题出在哪里呢?

这种模式有几个陷阱,你应该非常清楚。

第一个也是最明显的问题是,如果要恢复的对象很大,那么保存快照历史记录可能会很麻烦。解决这个问题的一种方法是只存储更改的差异,但这仅适用于您确切知道要应用快照顺序的情况(例如在撤消/重做操作中)。

另一个更隐蔽的问题是,如果快照创建不正确,则在遍历历史记录时很容易创建和累积错误。让我们举一个例子来说明这种情况。

假设你有一个史上最蠢的游戏:每次点击按钮就能获得 10 分,如果分数达到 100 分就能获得一个徽章。我们想在这里实现一个撤销机制,所以每次点击变量时都会存储快照score

我们点击 100 次,获得一个徽章,我们撤消,重新点击,获得第二个徽章。

错误功能

为什么会这样?因为我们忘了记录快照中的徽章,所以在撤销操作时,我们只是还原了分数,而没有清理徽章列表。

请少说话,多行动

您可以在此处找到这些示例的更详细版本

GitHub 徽标 shikaan /设计模式

设计模式在实际代码中的使用示例







终于到了编码时间!

正如我在介绍中所承诺的,我将展示如何通过 Command 和 Memento 解决相同的撤消问题。

免责声明:
我决定不在这个例子中使用 Electron,原因很简单,因为它会让不熟悉 Electron 的人觉得整个过程更加复杂,而且对 Electron 专家来说也没有任何价值。如果你对此感到不满,请留言,我也会添加那个例子。

该示例是一个非常简单的 React 应用程序,它应该是一个游戏:对图块进行排序以获胜。

它基本上设置一个监听器keyDown,并基于此调用一个方法(Memento)或发出一个命令(Command)。

在 Memento 的例子中,我们有一个Game组件负责处理所有游戏逻辑:移动图块、选择图块、计算用户是否获胜……这使得它成为了完美的Originator 组件,因为它也是我们存储可能想要通过撤销操作恢复的状态的地方。作为 Originator 组件还意味着它负责创建和恢复Snapshots 。

Snapshot当然是Memento,并且它对于GameES6 模块来说是“私有的”,以防止KeyboardEventHandler(又名CareTaker)知道它。

在 Command 示例中,我们增加了一个组件:CommandManager充当InvokerGame 。和的角色KeyboardEventHandler保持不变,但由于实现不同,它们的操作也有所不同。Gamenow 是命令的接收者KeyboardEventHandler,而是Client,即 的唯一所有者Command

您可能已经注意到,我们可以在这里互换使用CommandMemento,因为我们封装的操作(moveSelectedTile)是一个纯操作,没有副作用,所以我们实际上并不一定需要Snapshot 来重建状态:应用逆函数就足够了。

这是否意味着 Memento 和 Command不能共存?绝对不是。事实上,你可以将takeSnaphot方法封装在 Command 中,从而解耦CareTakerOriginator。或者,你也可以moveSelectedTile像我们之前做的那样,在 Command 中除了执行方法之外,还可以进行快照操作。最后一种是让 Command 和 Mememto 共存的最常见方法。

你可以从这个仓库开始,先尝试一下,作为练习。如果你心怀不轨,想破坏大家的兴致,可以提交 PR。

最后的话

嗯,随着我们开始添加知识点,把牌混搭起来,事情开始变得有趣起来了。随着时间的推移,情况肯定会有所改善,所以坚持下去吧 :D

如果您有任何反馈(“不要告诉我如何编码。你不是我的亲生妈妈!”),意见(“你的代码很糟糕,但你的表情很棒”),评论(“是的,好吧,行为模式很酷,下一步是什么?”),请留言或发表评论,让我们一起让这个系列变得更好。

直到下次!


1.如果你不确定什么是行为模式,请查看这里

2.为了避免忘记这一点,你应该记住“mem ento”和“memory ”有着相同的起源。这是一个记忆技巧,用来记住与记忆相关的内容。太棒了!

文章来源:https://dev.to/shikaan/design-patterns-in-web-development---2-memento-253j
PREV
如何将 ESP32-CAM 与 MicroPython 结合使用
NEXT
Next.js 15 身份验证