时间模式

总结了各种模式,您可以使用这些模式来回答有关过去信息状态的问题。这些问题包括“马丁在 1999 年 7 月 1 日的地址是什么”和“我们在 1999 年 8 月 12 日给他寄送账单时,认为马丁在 1999 年 7 月 1 日的地址是什么”形式的问题。

2005 年 2 月 16 日



这是我在 2000 年代中期撰写的《企业应用程序架构进一步发展》的一部分。遗憾的是,从那以后,太多其他的事情引起了我的注意,所以我没有时间进一步研究它们,而且在可预见的未来,我也看不到有多少时间。因此,这份资料还处于草稿阶段,在我能够找到时间再次进行处理之前,我不会进行任何更正或更新。

事物是会改变的。如果我们存储有关世界的信息,这可能不是问题。毕竟,当某些东西发生变化时,计算机化记录系统的一大价值在于它允许我们轻松更新记录,而无需诉诸液体纸张或重新输入页面信息。

然而,当我们需要记录变化的历史时,事情就变得有趣了。我们不仅想知道世界的现状,还想知道六个月前的世界状况。更糟糕的是,我们可能想知道两个月前我们认为六个月前的世界状况是什么样的。这些问题将我们带入了一个迷人的时间模式领域,这些模式都与组织对象有关,使我们能够轻松找到这些问题的答案,而不会完全扰乱我们的领域模型。在对象建模的所有挑战中,这既是最常见也是最复杂的挑战之一。

解决这个问题的最简单方法是使用审计日志。在这里,您关心的是保留更改记录,但您不希望经常回头使用它。因此,您希望它易于创建并且对您的工作影响最小。当有人需要查看它时,您预计他们将不得不做很多工作来挖掘信息。如果他们不需要经常这样做,也不需要快速获得结果信息,那么这很好。事实上,如果您使用的是数据库,那么它是免费的。

当信息必须更容易访问时,您需要做更多的工作。在这种情况下,人们最常用的模式是有效性。这只是用对象被认为有效的时间段来标记对象(图 1)。然后,当您操作对象时,您可以使用此时间段在正确的时间获取正确的对象。

图 1:使用显式日期范围来显示有效性。

有效性的问题在于它很明显且明确。在所有上下文中使用该对象的每个人都必须意识到时间方面。这会使模型变得相当复杂。因此,如果时间问题普遍存在,那么在不需要它们时隐藏它们,并在需要它们时使它们更易于使用通常是有意义的。

如果对象的属性很少需要容易访问的时间信息,则可以在这些对象上使用时间属性时间属性的要点是删除明显的有效日期,而是使用看起来像常规属性的内容,但使用将日期作为参数的访问器。这允许您询问“此属性在 2000 年 4 月 1 日的值是多少”。

图 2:对地址使用时间属性。

时间属性的本质是拥有一个由日期参数化的访问器,这使得该属性的用户不必浏览一堆具有有效性的对象。因此,时间属性有效性不是互斥的模式。您可以使用地址使用对象(使用有效性)来实现时间属性接口。如果地址使用对象带有其他职责,您可以提供访问那些需要这些其他职责的客户端的地址使用的方法,以及为那些发现更方便的客户端提供时间属性接口。

时间属性引入了使用基于时间的索引来引用事物的概念,但仅限于单个属性。如果对象上有许多时间属性,事情可能会变得很笨拙。有两种方法可以解决这个问题。第一种是使用快照,它为您提供一个对象,该对象引用真实对象在某个时间点的状态。因此,如果您想在 2000 年 4 月 1 日询问有关客户的许多问题,您可以请求该客户在 2000 年 4 月 1 日的快照,然后询问属性的值,而不必一直重复日期。

图 3:快照显示对象在某个日期的视图。

快照允许您查看对象在某个日期的视图,但有时您真的希望将对象视为随着时间的推移通过显式版本进行更改。这种愿望导致了时间对象时间对象有多种形式,但最直接的形式有两个类,如图 4所示。契约类是通常由其他对象引用的类,因为它表示随时间推移的契约。它包含一个版本的集合,这些版本记录了每次更改时契约的状态。这允许人们引用不变的联系人概念,或特定时间点的特定版本。

图 4:时间对象具有明确的版本历史记录,因此每次更改都会导致一个新版本。

在实践中,我看到有效性被广泛使用,但这通常是因为人们对时间属性和其他更复杂的模式不够熟悉。像时间属性时间对象这样的模式之所以有效,是因为它们隐藏了时间行为的大部分机制。然而,有效性仍然很重要:当人们想要一个仅在特定时间段内有效的显式对象时,它是正确的选择。

时间的维度

正如我上面所描述的,时间在建模中是一个非常具有挑战性的概念。然而,我跳过了时间模型中最尴尬的方面。我们都学过,即使只是从糟糕的科幻小说中,时间是第四维度。问题是这是错误的。

我发现描述这个问题的最佳方法是举个例子。假设我们有一个工资单系统,该系统知道员工从 1 月 1 日开始的工资率为 100 美元/天。2 月 25 日,我们按此费率运行工资单。3 月 15 日,我们了解到,从 2 月 15 日起,该员工的工资率更改为 211 美元/天。当我们被问及 2 月 25 日的工资率是多少时,我们应该如何回答?

从某种意义上说,我们应该回答 211 美元,因为现在我们知道那是当时的工资率。但我们通常不能忽视,在 2 月 25 日,我们认为工资率是 100 美元,毕竟那是我们运行工资单的时候。我们打印了一张支票,寄给了他,他兑现了。这些都是根据他的工资率发生的。如果税务机关询问我们他在 2 月 25 日的工资率,这就变得很重要了。

事实上,我们可以认为,丁斯代尔的工资率确实有两个历史对我们很重要。我们现在知道的历史,以及我们在 2 月 25 日知道的历史。事实上,总的来说,我们可以说,不仅丁斯代尔的工资率在过去的每一天都有历史,而且丁斯代尔的工资率的历史也有历史。时间不是第四维度,它是第四和第五维度!

我认为第一个维度是实际时间:事情发生的时间。第二个维度是记录时间,即我们知道它发生的时间。每当发生什么事时,总是有这两个时间伴随着它。丁斯代尔的加薪的实际日期是 2 月 15 日,记录日期是 3 月 15 日。同样,当我们询问丁斯代尔的工资率是多少时,我们真的需要提供两个日期:记录日期和实际日期。

记录日期实际日期丁斯代尔的工资率
1 月 1 日1 月 1 日100 美元/天
2 月 25 日2 月 25 日100 美元/天
3 月 14 日2 月 25 日100 美元/天
3 月 15 日1 月 1 日100 美元/天
3 月 15 日2 月 25 日211 美元/天

我们可以这样理解这两个维度。实际历史是在实际时间中回顾。如果我查看我当前的实际历史,我会看到丁斯代尔的工资在 2 月 15 日之前是 100 美元,在那之后增加到 211 美元。然而,这是今天在记录时间中的实际历史。如果我查看 2 月 25 日的实际历史,那么丁斯代尔从 1 月 1 日起就按 100 美元的工资支付,而 211 美元从未出现过。记录时间中的每一天(严格来说是每个时间点)都有一个实际历史。这些历史是不同的,因为我们发现我们过去认为是真实的事情不再是真实的。

从另一个角度来说,我们可以说实际历史中的每一天都有一个记录历史。记录历史告诉我们我们对那一天的了解是如何随着时间的推移而改变的。因此,实际时间中的 2 月 25 日有一个记录历史,表明直到 3 月 15 日,丁斯代尔的工资都是 100 美元,在那之后达到了 211 美元。

让我们进一步举例说明,假设我们在 3 月 26 日的工资单运行中进行了相应的调整。4 月 4 日,我们被告知该员工的先前信息有误,实际工资率在 2 月 15 日更改为 255 美元。现在我们如何回答“该员工在 2 月 25 日的工资率是多少?”这个问题。

我见过经验丰富的开发人员在面对这种事情时抓狂。但是,一旦您意识到一切都归结为这两个维度的概念,事情就会开始变得简单多了。一种可视化的方法是扩展前面的表格

记录日期实际日期员工工资率
1 月 1 日1 月 1 日100 美元/天
2 月 25 日2 月 25 日100 美元/天
3 月 14 日2 月 25 日100 美元/天
3 月 15 日1 月 1 日100 美元/天
3 月 15 日2 月 25 日211 美元/天
3 月 26 日2 月 25 日211 美元/天
4 月 4 日1 月 1 日100 美元/天
4 月 4 日2 月 25 日255 美元/天

如果我们查看我们当前的实际历史(即记录日期为今天的实际历史),我们会说丁斯代尔的工资从 1 月 1 日起是 100 美元,并在 2 月 15 日上升到 255 美元。对于当前的实际历史,211 美元的工资率根本没有出现,因为它从来就不是真实的。如果我们查看 3 月 26 日的实际历史,我们会看到丁斯代尔的工资在 2 月 15 日之前是 100 美元,在那之后上升到 211 美元。在 3 月 26 日的实际历史中,255 美元的工资率从未发生过,因为我们当时还不知道。

我们还可以考虑 2 月 25 日的记录历史。现在,这个记录历史表明,工资率是 100 美元(在那一天),直到 3 月 15 日才变为 211 美元。然后在 4 月 4 日,它又变回了 255 美元。

一旦您意识到这两个维度,就更容易思考这个问题了,但想到要实现这种东西就令人害怕。幸运的是,您可以做一些事情来简化实现方面的问题。

第一个简化是使用审计日志来应对这些更改并不困难。您需要做的就是在日志的每个条目中记录记录日期和实际日期。这个简单的练习足以使任何日志在两个维度上都保持有效,而且我相信即使您只关心其中一个维度,也值得这样做。

第二个简化是,您通常不希望您的模型同时处理两个维度。这里重要的是要知道您的模型中包含哪个维度,以及您将哪个维度留给审计日志

如果我们想保留事物的历史记录,想知道事物是如何随时间变化的,但并不关心我们何时了解到变化,我们会说这是*实际时间*。因此,如果我保留员工地址的记录,我可能会选择将其保留为实际时间属性。对于用于帮助在线查询的信息系统来说,这很有效,因为当您访问数据库时,您通常想知道的是实际历史记录。

当您有一个系统根据对象的状态执行诸如生成账单之类的操作时,就会出现记录时间事实。这些事情会导致关于账单如何计算的问题,这导致您需要知道在计算账单时对象的认为状态是什么。记录时间事实通常可以与软件中的版本控制系统相比较,在版本控制系统中,您可以回溯并说“4 月 1 日这个文件是什么样的?”

当然,有时您需要同时使用两个维度——这些被称为双时态事实。本质上,双时态信息始终需要两个日期。

双时态性是完整的解决方案,但始终值得考虑解决方法。账单计算就是一个例子。如果您想找出账单为何是某个金额,一种可能性是拥有一个完全双时态的数据库。但是,通常最好在计算账单时存储计算的详细跟踪。这以比双时态对象模型简单得多的方式满足了需求。

更新时间记录

到目前为止,我只讨论了时间信息在访问信息方面的应用,而不是在更新信息方面的应用。您如何允许更新会导致更多决策,但其中许多决策都涉及有用的简化。

一般来说,更改时间记录非常麻烦。如果我们有一个员工的工资从 2 月 15 日到 4 月 15 日是 211 美元,那么完全通用的更新将允许更改开始日期、结束日期和值的任意组合。为此提供接口很麻烦,因为它要求客户端非常了解时间信息是如何工作的。

第一个简化是您可能只有*附加*更改。附加更改始终添加到记录的末尾。附加更改的一个例子是“使员工从 2 月 15 日起生效的工资为 211 美元”。虽然乍一看,这似乎只是从组合中删除了一条数据,但结果实际上是大大简化了更新。实践中发生的大多数更新都是附加的,因此可以极大地简化客户端。此外,您可以通过附加更新的某种组合进行任何更改。虽然这对于具有复杂历史记录的对象来说会变得非常混乱,但此属性可以通过允许您仅支持附加接口来简化具有简单历史记录的对象。

第二个简化是只允许当前更新。当前更新仅允许对记录进行更改,其生效日期为今天。一般来说,即使是附加更改也可以发生在过去或未来的任何日期。当前更改根本不需要日期信息,从而允许您拥有一个完全非时间性的更新接口。

当前更新似乎好得令人难以置信,如果您只使用当前更新来更新时间信息,那么为什么要使用时间信息呢?好消息是记录时间信息只能使用当前更新进行更新。对记录时间信息进行追溯更改不仅会破坏记录的完整性,而且没有任何情况需要追溯更改,而这些更改无法通过实际时间维度来处理(除非欺诈是您的要求之一)。这是一个很大的进步,因为它意味着整个维度都有不可见的更新——您只需要在查询时担心记录时间,而在更新时则不需要。

其他读物

由于这是一个复杂的领域,因此随着人们对这些问题的探索,它产生了一些其他的著作。对该领域最全面的论述是Snodgrass。他的工作基于关系数据库,并且本书的大部分内容都是关于如何在 SQL 中处理这些问题。然而,问题是一样的。此外,他所做的许多评论和他使用的术语与即将发布的 SQL 标准中关于时间数据库的材料相同。这本书现在已经绝版了,但您可以从Richard Snodgrass 的出版物页面获得 pdf 版本。

术语问题是我仍然不太满意的问题。他使用了我使用的两个维度,但他的术语不同。我所说的实际时间,他称之为*有效时间*,我所说的记录时间,他称之为*事务时间*。在这些模式的早期版本中,我遵循了他的术语,但许多人说他们发现这些术语很难理解。因此,我决定使用不同的术语。另一个区别是,他将随时间变化的表称为*排序的*。当然,对象始终使用序列,而不仅仅是用于时间目的,因此我将随时间变化的事物称为时间性的。

[Anderson] 是时间模式的最佳集合之一。我再次借鉴了他的想法,但改变了一些术语。我使用了*版本*而不是*版本*来表示某个时间段内属性或对象的值。他的*更改日志*和*关联历史记录*模式与时间属性相同。我认为这两种 Anderson 模式之间的区别更多的是实现问题。他的*自身历史记录*模式与时间对象相同。

同样在PLoPD 4一书中,您会发现由 Andy Carlson 与我和 Sharon Estepp 合作撰写的一篇论文 [Carlson et al]。这给出了时间属性快照的早期描述。它还提到了一种模式*时间关联*,我现在将其视为有效性的应用。

同样提交给了 PLoP,但目前尚未公开的是[Arnoldi et al]。这引入了一些我在这里的模式中尚未完全探索的有趣想法(至少现在还没有)。

[Anderson] 和[Arnoldi et al] 都考虑使用时间点以外的对象作为时间记录的索引。[Anderson] 使用(单时态)事件,而[Arnoldi et al] 使用*视角*:本质上是将两个时间点组合成一个对象。尽管这两种方法都有可取之处,但我在这里没有使用它们,因为我觉得使用时间点可以完成您需要的大部分工作,而且解释起来更简单。


重大修订

2005 年 2 月 16 日:将这些模式移至 EAA 开发部分。

2001 年 1 月 15 日:将二维术语从有效/事务更改为实际/记录。

2000 年 8 月 28 日:初稿