4.6 确保系统状态完整性
你:函数在计算阶段处理不变数据的方式仍然困扰着我:我们如何保持数据完整性?
乔:你是什么意思?
你:在面向对象中,数据只由与数据属于同一类的方法操作。它可以防止其他类损坏类的内部状态。
乔:你能给我举一个图书馆失效的例子吗?
你:例如,假设突变的代码将一个图书项目添加到成员的图书借阅中,而没有在catalog中将该图书项目标记为已借出。那么系统数据就会被破坏。
乔:在DO中,我们有权确保整个系统级别的数据完整性,而不是将验证分散在许多类中。
你:我不明白。
乔:提交阶段的代码对于所有变化都是通用的,这一事实允许我们在中心位置验证系统数据:在提交阶段的开始,有一个步骤检查(请参见清单4.8)要提交的系统状态的版本是否有效。如果数据无效,提交将被拒绝。
清单4.8 提交阶段内的数据验证
SystemState.commit = function(previous, next) {
if (!SystemValidity.validate(previous, next) {
throw "The system data to be committed is not valid!";
});
this.systemData = next;
}
你:听起来很像git中的提交Hook。
乔:我喜欢你的比喻!
你:为什么要将前一个传递给SystemValidity.valify(),而不是传递给下一个呢?
乔:因为它允许SystemValidity.valify()的代码在计算方面优化验证。例如,我们可以只验证已更改的数据部分。
TIP 在DO中,我们将系统数据作为一个整体进行验证。数据验证与数据操作分离。
你:SystemValidity.valify()的代码是什么样子的?
乔:我会在第二部分向你展示,例如,我们如何确保图书记录中提到的每个作者的身份都是有效的。它涉及更高级的数据操作逻辑。
4.7 时间旅行
通过结构共享操作不变数据的多版本状态方法的另一个优点是,我们可以跟踪数据的所有版本的历史记录,而不会破坏程序的内存。例如,它允许我们非常容易地将系统恢复到较早的状态。
你:你之前告诉我,把系统恢复到以前的状态很容易。你能教我怎么做吗?
乔:很乐意。但在此之前,我想确保您理解为什么跟踪所有版本的数据在内存方面是高效的。
你:我认为这与不可变函数使用结构共享的事实有关。并且该状态的后续版本之间的大部分数据是共享的。
TIP 结构共享允许我们保留系统状态的多个版本,而不会导致内存爆炸。
乔:太好了。现在,我将向你们展示撤销突变是多么简单。为了实现撤消,我们的SystemState类需要有两个对系统数据的引用:对系统当前状态的systemData引用和对系统以前状态的previousSystemData引用。
你:这是有道理的。
乔:在提交阶段,我们更新previousSystemData和systemData。
你:要实现撤销需要做些什么呢?
乔:Undo是通过让systemData引用与previousSystemData相同版本的系统数据来实现的。
你:你能给我举个例子吗?
乔:为了简单起见,我会给每个版本的系统状态一个数字。它从V0开始,每次提交突变时,版本都会递增:V1、V2、V3等…
你:好的。
乔:假设目前我们的系统状态是V12(参见图4.6)。在SystemState对象中,systemData指的是V12,previousSystemData指的是V11。
你:到目前为止还不错。
乔:现在,当提交一个突变(例如,添加一个成员)时,两个引用都向前移动:systemData指的是V13,previousSystemData指的是V12
你:我想,当我们撤销突变时,两个引用都会向后移动。
乔:理论上是这样的。但在实践中,它需要维护所有状态引用的堆栈。目前,为了简化操作,我们只保留对前一个版本的引用。因此,当我们撤销突变时,两个引用都引用了V12,如图4.8所示。
你:你能告诉我如何实现这个撤销机制吗?
乔:实际上,只需要对SystemState类做几处更改。结果如清单4.9所示。请注意Commit()函数中的更改:我们在systemDataBeforeUpdate中保留了对系统当前状态的引用。如果验证和冲突解决成功,我们将同时更新previousSystemData和systemData。
清单4.9 具有撤消功能的SystemState类
class SystemData {
systemData;
previousSystemData;
get() {
return this.systemData;
}
commit(previous, next) {
var systemDataBeforeUpdate = this.systemData;
if (!Consistency.validate(previous, next) {
throw "The system data to be committed is not valid!";
});
this.systemData = next;
this.previousSystemData = systemDataBeforeUpdate;
}
undoLastMutation() {
this.systemData = this.previousSystemData;
}
}
你:我看到实现System.undoLastMutation()仅仅是让systemData引用与previousSystemData相同的值。
乔:就像我告诉你的,如果我们需要允许多次撤销,代码会更复杂一些。但你明白我的意思了。
4.8 总结
在本章中,我们探索了DO如何通过多版本方法管理状态,在多版本方法中,变异被分成计算和提交两个阶段。
在计算阶段,使用不可变的函数操作数据,这些函数利用结构共享来高效地(内存和计算)创建新版本的数据,其中共享两个版本之间通用的数据,而不是复制。
接下来,状态引用发生在提交阶段,这是我们系统中唯一有状态的部分。提交阶段的代码对所有突变都是通用的,这一事实允许我们在更新状态之前在中心位置验证系统状态。
此外,保存系统数据版本的历史记录也很容易和高效,并且可以直接将系统恢复到以前的状态。作为一个例子,我们已经了解了如何在一个撤消系统中实现撤消。