设计模式
设计模式在实际代码中的使用示例
这些是本系列文章的参考资源
你们可能还记得,在试播集里,我说过要用三个例子来解释Command :一个 UI Kit、一个 CQRS 应用和一个 Electron 中的撤销/重做实现。但在Command 集中,我没有提供后者,原因很简单:我是个混蛋。
此外,对我来说,用这个例子来解释属于四人帮经典模式的另一种行为模式1:Memento更有意义。
假设你正在使用计算器。你提供一个表达式,它会为你进行计算。为了简单起见,我们只考虑它的一个方法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;
}
有一天,我们决定为这个应用程序实现一个撤销机制。实现这个机制的第一个想法可能是简单地应用刚才操作的逆函数。
不幸的是,这对于该功能不起作用power
。
例如:撤消y = power(x, 2)
将应用sqrt(y, 2)
,但power(2, 2)
和会power(-2, 2)
产生相同的结果,因此您不能x
仅通过使用就明确地获得y
。
此时,当您calculate
和时将先前的状态保存在快照中undo
,然后使用这样的快照重置计算器的状态看起来更简单,更有效。
Memento提供了一种巧妙的方法来解决这个问题。
意图
在不违反封装的情况下,捕获并外部化对象的内部
状态,以便对象稍后可以恢复到该状态。
是的,您刚刚赢得了这一轮“猜引言”:这句话来自“四人帮”。
这里的想法非常简单:我们希望有一种系统的方法来存储给定对象的内部状态的快照,而不暴露这种状态,以便以后能够恢复。
如果你还在疑惑为什么不应该暴露状态,或许你仍然没有像应该的那样担心耦合。这确实很糟糕。不过,你仍然可以通过阅读这篇文章来解决这个问题。我会在这里等你。
...
完成了吗?我们可以开始实践一下Memento 了。
首先要说的是:为什么这个模式叫做 Memento?Memento是一个拉丁词,可以安全地翻译成“提醒者2”Calculator
。这是我们用来存储我们感兴趣的部分状态的对象。
Calculator
,即状态的起源,被称为Originator,而这个故事的第三个角色将负责使整个事情正常运转,被称为CareTaker。
总而言之,Memento 中的参与者及其职责如下:
实际上,这些将会变成类似这样:
// 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)
}
}
有了Memento,我们算是幸运的:你不需要寻找极其复杂的用例来实现它。撤消/重做场景是该模式迄今为止最常发挥作用的地方,但每次你需要将对象恢复到上一个阶段时,它都可以轻松地重复使用。
您需要另一个例子,不是吗?
假设你有一个 Web 应用的个人资料页面。用户点击了“编辑个人资料”,但在执行了一些操作后,他们“取消”了操作。除非你每次都想通过 AJAX 调用重新获取用户信息,否则一个好主意是存储一个备忘录,其中包含用户个人资料的快照,以便在取消操作后恢复。
Memento是实现这一目标的唯一方法吗?不是。在这种情况下,另一个相当常见的模式是Prototype,它可能是下一集的主题。也可能不是,谁知道呢?无论如何,你现在需要了解的是, Prototype提供了另一种创建对象状态副本的方法,只不过方式不同。
总而言之,当您必须回顾对象的历史时,拍摄快照可以让您的生活变得更轻松。
你的下一个问题可能是:这仅仅是方便还是有必要?我们在计算器的例子中看到,有时反转上一个操作并不足以恢复到之前的状态。不幸的是,这不仅适用于不可逆的数学函数,而且适用于任何方法产生副作用的情况。在这种情况下,通常情况下,创建快照是安全地恢复到之前状态的唯一方法。
这种模式有几个陷阱,你应该非常清楚。
第一个也是最明显的问题是,如果要恢复的对象很大,那么保存快照历史记录可能会很麻烦。解决这个问题的一种方法是只存储更改的差异,但这仅适用于您确切知道要应用快照顺序的情况(例如在撤消/重做操作中)。
另一个更隐蔽的问题是,如果快照创建不正确,则在遍历历史记录时很容易创建和累积错误。让我们举一个例子来说明这种情况。
假设你有一个史上最蠢的游戏:每次点击按钮就能获得 10 分,如果分数达到 100 分就能获得一个徽章。我们想在这里实现一个撤销机制,所以每次点击变量时都会存储快照score
。
我们点击 100 次,获得一个徽章,我们撤消,重新点击,获得第二个徽章。
为什么会这样?因为我们忘了记录快照中的徽章,所以在撤销操作时,我们只是还原了分数,而没有清理徽章列表。
您可以在此处找到这些示例的更详细版本
终于到了编码时间!
正如我在介绍中所承诺的,我将展示如何通过 Command 和 Memento 解决相同的撤消问题。
免责声明:
我决定不在这个例子中使用 Electron,原因很简单,因为它会让不熟悉 Electron 的人觉得整个过程更加复杂,而且对 Electron 专家来说也没有任何价值。如果你对此感到不满,请留言,我也会添加那个例子。
该示例是一个非常简单的 React 应用程序,它应该是一个游戏:对图块进行排序以获胜。
它基本上设置一个监听器keyDown
,并基于此调用一个方法(Memento)或发出一个命令(Command)。
在 Memento 的例子中,我们有一个Game
组件负责处理所有游戏逻辑:移动图块、选择图块、计算用户是否获胜……这使得它成为了完美的Originator 组件,因为它也是我们存储可能想要通过撤销操作恢复的状态的地方。作为 Originator 组件还意味着它负责创建和恢复Snapshot
s 。
Snapshot
当然是Memento,并且它对于Game
ES6 模块来说是“私有的”,以防止KeyboardEventHandler
(又名CareTaker)知道它。
在 Command 示例中,我们增加了一个组件:CommandManager
充当InvokerGame
。和的角色KeyboardEventHandler
保持不变,但由于实现不同,它们的操作也有所不同。Game
now 是命令的接收者KeyboardEventHandler
,而是Client,即 的唯一所有者Command
。
您可能已经注意到,我们可以在这里互换使用Command和Memento,因为我们封装的操作(moveSelectedTile
)是一个纯操作,没有副作用,所以我们实际上并不一定需要Snapshot 来重建状态:应用逆函数就足够了。
这是否意味着 Memento 和 Command不能共存?绝对不是。事实上,你可以将takeSnaphot
方法封装在 Command 中,从而解耦CareTaker
和Originator
。或者,你也可以moveSelectedTile
像我们之前做的那样,在 Command 中除了执行方法之外,还可以进行快照操作。最后一种是让 Command 和 Mememto 共存的最常见方法。
你可以从这个仓库开始,先尝试一下,作为练习。如果你心怀不轨,想破坏大家的兴致,可以提交 PR。
嗯,随着我们开始添加知识点,把牌混搭起来,事情开始变得有趣起来了。随着时间的推移,情况肯定会有所改善,所以坚持下去吧 :D
如果您有任何反馈(“不要告诉我如何编码。你不是我的亲生妈妈!”),意见(“你的代码很糟糕,但你的表情很棒”),评论(“是的,好吧,行为模式很酷,下一步是什么?”),请留言或发表评论,让我们一起让这个系列变得更好。
直到下次!
2.为了避免忘记这一点,你应该记住“mem ento”和“memory ”有着相同的起源。这是一个记忆技巧,用来记住与记忆相关的内容。太棒了!
文章来源:https://dev.to/shikaan/design-patterns-in-web-development---2-memento-253j