Temporal Patterns by Martin Fowler

摘自 http://www.martinfowler.com/eaaDev/timeNarrative.html

WORK-IN-PROGRESS: - this material is still under development
未完稿: - 这份材料仍在编写中

Temporal Patterns
时态模式

摘要:总结了用来表达关于某种信息的某个过去状态的各种模式。这包括了形如“Martin在1999年7月1日的地址是什么?”和“我在1999年8月12号寄了一份帐单给Martin, 那时(8月12日)我们手头上他在1999年7月1日的地址是什么?”之类的问题

最近重要更新: 2005年2月16日

世事常变。如果我们(只需要)储存关于现实世界的信息,这并不是个问题。毕竟当事物发生变化,电子化记录系统的一个重要优点是:它使我们很容易就能更新记录,而不用靠涂改液搭救或者重新打入好几页纸的信息。

但当我们需要记录变动的历史时,事情就变得有趣了。我们不仅仅想知道现实世界的状态,还想知道现实世界在半年前的状态。更糟的是,我们可能想知道两个月之前我们对“半年前的世界的状态”的了解。这些问题把我们带入了时效模式的迷人境地,这些模式都是通过组织对象,让我们能更容易地回答这些问题,而不会因此把业务模型搞得一团糟。在所有对象建模的问题中,这个问题是最常见而又是最复杂的。

解决这个问题最简单的方法是使用一个[Audit Log](查帐日志)。在此你希望能保留一份关于变动的记录,但你并不打算经常回过头去使用它。因此你希望它能易于创建而且最低限度地妨碍你的正常工作。当有人要看它的时候,那么你要做好心理准备他们需要做大量的工作去发掘(过去的)信息。如果他们并不需要经常这样做,而且不会要求在短时间内得出结果,那么这(个方案)不错。事实上如果你在使用数据库,它是没有什么额外花费的(译注:it's free, 可能是指数据库本来就有类似用Log回滚的功能)。

当信息需要更好的可访问性时,你就需要多做一点工作。在这种情况下,人们最常用的模式是[Effectivity](时效)。它其实是很简单地为对象加入一个表示有效期的标记。(图1)这样当你操作对象时就可以通过这个有效期来获取某个特定时间的特定对象。


Figure 1: Using an explicit date range to show effectivity.
图1:使用显式日期范围来表达时效性

[Effectivity]的问题在于它是显式的。在任何上下文中使用这个对象的人都必须意识到时态的存在。这会使整个模型的复杂性显著增加。因此如果时态问题(在模型中)普遍存在,那么往往需要在你用不着它们的时候把它们隐藏起来,而在你用得着它们的时候,让它们更方便使用。

如果只是对象中的一小部分属性需要访问时态性信息,那么你可以在这些对象中使用[Temporal Property]。[Temporal Property]的关键之处在于移除显式的有效期,而使用一些普通的对象属性,但这些对象属性接收一个日期参数。这就允许你提问类似“这个属性在2000年4月1日的值是什么?”这样的问题。


Figure 2: Using a temporal property for the address..
图2:使用[Temporal Property]的地址属性

[Temporal Property]的要点在于它具有一个以日期为参数的入口,这就把使用者从[Effectivity]那种必须在一系列对象(译注:指同一实体不同时效)中导航的状况中解脱出来。因此,[Temporal Property]与[Effectivity]并不是互斥的模式。你可以使用“地址使用对象”(使用[Effectivity])来实现一个[Temporal Property]接口。如果“地址使用对象”还承载着其他责任,你可以为需要这些服务的客户提供访问“地址使用对象”的(简单)方法。而[Temporal Property]接口则为需要方便(使用时态信息)的客户而设。

[Temporal Property]引入了这样一个概念:采用一个时间下标来引用事物。但到目前为止仅仅作为一个单一属性来看待。如果你在一个对象有许多个temporal properties, 就会显得很笨拙。有两个方法来应付这种情况。第一种是使用[Snapshot](快照), 它提供了一个关于实际对象在某一个时间点的状态的引用。这样,如果想问大量关于一个顾客在2000年4月1日的情况的问题,你可以先获取一个“关于这个顾客在2000年4月1日状况”的[Snapshot],然后获取有关属性的值,而不用一直重复指定同一个日期。


图3:一个[Snapshot]展示了一个对象在某一个日期的视图

[Snapshot]允许你获取一个对象在一个日期的视图,但有时你是想获取一个对象能清楚地反映出随着时间推移的版本变动。[Temporal Object](时态对象)就应运而生了。[Temporal Object]可能有很多种形式,但最直观的一种如图4所示,由两个类组成。其中 合同类(the contract class) 是通常被其他对象引用的,因为它代表了“在时间流中的合同”。它包含了一个版本的集合,这个集合记录了合同每次改动时的状态。这使人们既能引用不变的合同概念,又能指定某个日期的特定版本。


Figure 4: Temporal Objects have an explicit history of versions, so that each change leads to a new version.
图4:[Temporal Objects]具有清楚的历史版本记录,因此每次改动都产生新的版本

在实践中,我看到[Effectivity]被广泛采用,但通常这是因为人们对[Temporal Property]和其它更为老练的模式认识不深。[Temporal Property]和[Temporal  Object]的优点在于他们隐藏了大部分的时态机制。但说到底[Effectivity]仍然是很重要的:当人们希望显式获取仅在一段特定时间内有效的对象时,它是一个正确选择。

--------------------------------------------------------------------------------

Dimensions of Time
时间的维度

正如我在上文所说,时间在建模中是个很有挑战性的概念。但(在上面的描述中)我已经忽略了时态模型中更为笨拙的一面。我们都听说过——从一些三流科幻小说中——时间是第四维度。麻烦在于,这个说法是错误的。

阐述这个问题最好的方法是用一个例子。假设我有一个工资发放系统,它知道从1月1日起,一个雇员的工资是每天100元。在2月25日,我以这个工资标准运行了这个系统发放工资。到了3月15日,我发现事实上从2月15日起,雇员的工资已经变为了每天211元。那么当我们被问及2月25日当天雇员的工资标准是多少时,应怎样回答呢?

某种意义上,我们应该回答211元,因为现在我们知道这才是当时真正的工资标准。但我们不能忽略这个事实:在2月25日我们以为工资标准是100元。毕竟我们以这个标准运行了工资发放程序。我们打印了支票,发给了雇员,而且他们也兑现了支票。这一切运作都基于(当时认为的)工资标准进行。特别是如果税务部门问起雇员在2月25日的工资率,这个事实就很重要了。

实际上,我们可以认为有两个版本的工资标准历史对我们有用。我们现在已经知道的历史,以及我们在2月25日所知的历史。一般而言,事实上我们可以认为不仅存在着过去工资率的历史(译注:以今日为准),还存在着每日工资率历史的历史(译注:-_-|||)。时间不仅是第四维度,还是第五维度!

我把其中第一个维度看作是实际时间(actual time): 某件事发生的实际时间。第二个维度是记录时间,我们知道这件事的时间。任何事发生的时候, 总是伴随着这两个时间。上面例子中工资上涨的实际时间是2月15日,记录时间是3月15日。相应地,当我们需要获取工资率是多少的时候,我们事实上应该提供两个日期:记录日期与实际日期。

record date actual date Dinsdale's rate
记录日期 实际日期 工资率(元/天)     译注
1月1日    1月1日  100             表示在1月1日的记录中,1月1日的工资率是100
2月25日   2月25日 100             表示在2月25日的记录中,2月25日的工资率是100
3月14日   2月25日 100             表示在3月14日的记录中,2月25日的工资率是100
3月15日   1月1日  100             表示在3月15日的记录中,1月1日的工资率是100
3月15日   2月25日 211             表示在3月15日的记录中,2月25日的工资率是211

我们可以这样去看待这两个维度。实际历史是从实际时间角度回顾。从我的当前实际历史,我能看到工资直到2月15日之前都是100元, 然后在这一天上升到211元。但这是以今天为记录时间的实际历史。如果我是看2月25日的实际历史,那么工资率就是从1月1日起保持100元不变,211元这个数字根本没有出现过。在记录时间中的任何一天(严格来说,任何时刻)都具有一个(独立的)实际历史。随着我们发现之前认为是真的事情其实并不完全真实,这些历史就会出现差异。

从另一个角度,我们可以说实际历史中的每一天都具有一个(独立的)记录历史。这个记录历史表示着我们对于这一天的看法是如何随时间而改变的。因此,在实际时间中的2月25日有一个记录历史,表明了在3月15日之前,我们一直认为该天(2月25日)工资率是100元,而在3月15日这天, 我们认识到2月25日的工资是211元.

让我们把这个例子更进一步。假设我们(因为上面的改动)对3月26日的工资发放标准作了相应的调整。但在4月4日,我们得知我们之前收到的通知有错,其实在2月15日那天工资实际上是提高到了255元。现在,我们应该怎样回答“在2月25日的工资率是多少?”这个问题呢?

我曾经见过一些资深的开发人员被类似这样的问题弄得头崩额裂。但一旦你意识到任何事都可以归纳到这种“二维时间轴”的理念中,事情就会简单得多了。例如可以这样扩展我们之前的表:

record date actual date Dinsdale's rate
记录日期 实际日期 工资率(元/天)     译注
1月1日    1月1日  100             表示在1月1日的记录中,1月1日的工资率是100
2月25日   2月25日 100             表示在2月25日的记录中,2月25日的工资率是100
3月14日   2月25日 100             表示在3月14日的记录中,2月25日的工资率是100
3月15日   1月1日  100             表示在3月15日的记录中,1月1日的工资率是100
3月15日   2月25日 211             表示在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日的实际历史,我们会看到工资率从最初的100元,在2月15日升到了211元。在3月26日的实际历史中,255元的工资率根本没有出现过,因为我们在当时还不知情。

我们还可以看看2月25日的记录历史。现在,这个记录历史说明了(人们所认为该日的)工资率在3月15日之前是100元,然后变成了211元,直到4月4日,又变成了255元。

一旦你意识到这两个二维度,你在思考这类问题时就会容易得多。但当你考虑到要(在设计中)实现这类东西,又会头痛起来。幸运地,在具体实现时,有很多方法可以简化这个问题。

第一个简化的方法是,通过使用[Audit Log]来处理这些变动并不难。你只需要在日志中记录每一个实体(变动)的记录日期与实际日期。这个简单的热身运动足以使日志在两个维度中都生效,而且我觉得即使你只关心一个维度,也值得这样做。

第二个简化是通常你并不想让你的模型同时处理两个维度。这里的关键点是,你要分清楚你要把哪个维度放进模型中,而另一个维度则只让[Audit Log]去管理。

如果我们只希望通过保存历史信息来知道事物是如何随时间改变,而不关心我们什么时候得知这些改变,这可以称为“实际时态” (actual-temporal)。如果我要保存雇员的地址,我可能会选择“实际时态”属性的方式。对于用来在线查询的信息系统,这种方式工作得很好。因为当你访问数据库,你通常只关心“实际历史”

“记录时态要素”出现于当你需要这样一个系统的时候,它的职责类似于基于一些对象的状态生成一些帐单。这些职责会引出“这些帐单是怎样计算出来的”一类的问题,这又导致你需要知道在计算这些帐单的时候,你所知的相关对象的状态是怎样的。“记录时态”就象软件开发中的版本控制系统,你可以回过头去说“这个文件在4月1日是怎么样的?”

当然,有时你会同时需要两个维度——这称为“双时态要素”。本质上,双时态信息总是同时需要两个日期的。

双时态是完整的解决方案,但想一些办法去绕过它总是物有所值的。在帐单计算中有一个例子。如果你需要知道一张帐单是怎样算出来的,其中一个做法是使用完整双时态的数据库设计。但是,通常采用把计算过程细节完整记录下来的做法会更好。这个做法因为与原始需求(本质上)相似,比双时态对象模型更能满足需求。

--------------------------------------------------------------------------------

Updating a Temporal Record

So far I've only talked about temporal information in terms of accessing information, not in terms of updating it. How you allow your updates leads to more decisions, but many of these involve useful simplifications.

In general changing the time record is quite messy. If we have an employee whose rate was $211 from Feb 15 to Apr 15, a fully general update will allow to change any combination of start date, end date, and value. Providing an interface for this is awkward since it requires the client to know a lot about how temporal information works.

The first simplification is that you may have only additive changes. An additive change always goes onto the end of the record. An example of an additive change is "make the employees rate $211 effective Feb 15". Although at first blush this looks like you are only removing one piece of data from the mix, the consequences actually work out so that it greatly simplifies the update. Most of the updates that happen in practice are additive, so that can greatly simplify clients. Furthermore you can make any change with some combination of additive updates. While this would get horribly messy on an object with a complicated history, this property can simplify objects with a simple history by allowing you to only support an additive interface.

The second simplification is allowing only current updates. A current update allows changes in the record only with an effective date of today. In general even additive changes can occur at any date in the past or future. A current change requires no date information at all, allowing you to have an interface for updates that is completely non-temporal.

Current updates seem too good to be true, why have temporal information if you only update it with current updates? The good news here is that record-temporal information can only be updated with current updates. Not just would a retroactive change to record-temporal information break the integrity of the record, there's no situation that requires a retroactive change that can't be handled by the actual-time dimension (unless fraud is one of your requirements). This is a big step as it means that one whole dimension has invisible updates - you only have to worry about record time when querying, not when updating.


--------------------------------------------------------------------------------

Other Readings
Since this is a complicated area, it has generated some other writings as people have explored these problems. The most comprehensive treatment of this area is Snodgrass. His work is based on relational databases and most of the book is based on how to handle these issues in SQL. The problem, however, is the same. Furthermore much of the comments he makes and terminology he uses is the same as the forthcoming SQL standard's material on temporal databases.

The matter of terminology is one that I'm still not completely happy with. He uses the same two dimensions that I do, but his terms are different. What I call actual time he calls valid time, what I call record time he calls transaction time. In earlier versions of these patterns I followed his terms, but many people said they found these terms very difficult to follow. As a result I decided to use different terms. Another difference is that he refers to a table that changes over time as sequenced. Of course, objects use sequences all the time, not just for temporal purposes, so I've called things that change over time temporal.

[Anderson] is one of the best collections of temporal patterns. Again I've plundered ideas, but changed some terminology. I've used the term version rather than edition to mean a value for a property or object during some time period. His Change Log and History on Association patterns are the same as Temporal Property. I see the difference between the two Anderson patterns as more a matter of implementation. His History on Self pattern is the same as Temporal Object.

Also in the same PLoPD 4book, you'll find a paper [Carlson et al]written by Andy Carlson in collaboration with myself and Sharon Estepp. This gives earlier descriptions of Temporal Property and Snapshot. It also mentions a pattern Temporal Association which I now look at as a use of Effectivity.

Also submitted to PLoP, but not available publicly at the moment is [Arnoldi et al]. This introduces a number of interesting ideas that I haven't fully explored in the patterns here (at least not yet).

Both [Anderson] and [Arnoldi et al] consider using objects other than Time Point to act as indexes into the temporal record. [Anderson] uses a (single-temporal) event, and [Arnoldi et al] use a perspective: essentially two timepoints combined into a single object. Although there are things to be said for both of these approaches, I haven't used them here as I feel that using time points does most of what you need, and is simpler to explain.

Significant Revisions
16 Feb 05: Moved these pattern over to the EAA development section.

15 Jan 01: Changed bidimensional terms from valid/transaction to actual/record.

28 Aug 00: First draft

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值