时间点
表示某个时间点,具有某种粒度
2004 年 3 月 7 日
这是我在 2000 年代中期进行的 企业应用程序架构进一步开发 写作的一部分。不幸的是,此后太多其他事情吸引了我的注意力,所以我没有时间进一步研究它们,而且在可预见的未来我也看不到太多时间。因此,这些材料非常草稿形式,我不会进行任何更正或更新,直到我有时间再次处理它。
时间点,很明显,是时间中的一个点。我正在写这篇文章,时间是 2000 年 8 月 28 日。这是一个时间点。这就引出了一个问题,为什么要将时间点作为一种模式来写?毕竟它们现在是许多语言和大多数类库的一部分。
问题在于,时间点有一些微妙之处,即使是构建库的人也可能没有意识到。
工作原理
时间点最常见的问题是,它们以不同的精度出现。当我说我正在写这篇文章,时间是 2000 年 8 月 28 日,以及我说我正在写这篇文章,时间是 2000 年 8 月 28 日下午 2:33:34 时,我说的是两件不同的事情。一个陈述是精确到天,另一个是精确到秒。请注意,秒精度比天精度更精确,但在这种情况下,恰好不太准确。任何时间点都需要知道它的精度,以便您可以回答诸如此事件是否与另一个事件同时发生之类的疑问。
关键点是,您不能仅依靠大多数领域的精度。许多业务都是以天为精度进行的。我的手机转账请求在一天中的什么时候发生并不重要,它只是根据我进行转账的那一天进行处理。否则生活会变得非常烦人。如果我想在账单提交付款的同一天进行转账,我是否应该担心账单提交的确切时间。常见的商业惯例说,不,如果是在同一天,我就不会冒透支或拒绝付款的风险。
注意不要使用比您需要更精确的时间点。许多平台只提供精确到秒或更高精度的時間點。那么我如何表示 2000 年 8 月 28 日的任何时间?通常,您使用一种约定,例如在午夜“00:00:00”整。这在某些情况下可能有效,但问题会逐渐显现。将几毫秒泄漏到时间点中往往出奇地容易,这时您会遇到问题,因为 2000 年 8 月 28 日不等于 2000 年 8 月 28 日。
时间点的另一个棘手领域是如何处理时区。与精度类似,没有适用于所有应用程序的正确答案。有时您需要带有时区的时间点,有时不需要。没有时区的时间点是完全合理的,这意味着它在它所在上下文的本地时间内。您会在以下情况下找到它们:进一步的时区信息要么没有用,要么可以从它的上下文中获取。如果您不需要时区,请谨慎使用它。
我遇到的一个让我非常困扰的这个问题出现在微软的 Outlook 中。您输入的任何约会时间(至少在 Outlook 98 中)都是特定于时区的。因此,如果我移动时区并更改了笔记本电脑上的时间,会议时间也会改变(除非我在最初将时间输入 Outlook 时考虑到了这一点——大多数人不会这样做)。这加剧了这样一个事实:如果我选择了一整天会议,它不会将这一天作为一天精度的時間點,而是选择了一个 时间点范围。因此,我在波士顿输入的一整天会议,当我飞往芝加哥时,变成了从晚上 11 点到晚上 11 点的会议。更糟糕的是,我的 WinCE 机器只以天为精度显示一整天会议,使用范围的开始,从而将我的一整天约会移动到前一天。
在我的 时间点 讨论中,时间点的一个重要特征是它们是锚定的,也就是说,时区指的是时间线上特定的一点。值为下午 2:30 的时间对象没有锚定,因为它可能意味着任何一天的下午 2:30。如果有一个天精度的时间点来为下午 2:30 提供上下文,那么下午 2:30 与天精度时间一起表示一个(正确锚定的)时间点。
时间点类的一个显而易见且常见的服务是获取当前时间点。通常,这是通过使用操作系统查询系统时钟来完成的。但是,在这里添加间接性是一个好主意。测试通常需要稳定的时间,而且通常即使是操作也可能需要在星期一运行系统,就像上周五一样。因此,除了访问系统日期之外,还值得访问处理日期,它可能与系统日期相同,也可能不同。此外,处理日期应该在操作期间可设置。当您使用日志记录时,您可能需要在日志中使用处理日期和系统日期。
何时使用它
某种形式的时间点几乎无处不在。使用它们的关键问题是决定上面讨论的精度和时区问题。只有在您的域需要时才使用天精度的时间点,不要尝试使用约定或 范围 来伪造它,即使这意味着要编写自己的包装类。同样,除非您确实需要,否则不要使用时区。考虑一下使用系统的人如何看待世界,许多人除非需要,否则不会考虑时区。
示例:一个简单的日期精度包装器(Java)
这是我在这里用于示例的简单日期精度包装器的一部分。基本结构包装了一个 Java 格里高利日历实例
class MfDate...
private GregorianCalendar _base; public MfDate() { this(new GregorianCalendar()); } public MfDate(int year, int month, int day) { initialize (new GregorianCalendar(year, month - 1, day)); } private void initialize (GregorianCalendar arg) { _base = trimToDays(arg); } private GregorianCalendar trimToDays(GregorianCalendar arg) { GregorianCalendar result = arg; result.set(Calendar.HOUR_OF_DAY,0); result.set(Calendar.MINUTE, 0); result.set(Calendar.SECOND, 0); result.set(Calendar.MILLISECOND, 0); return result; }
请注意,我通过将格里高利日历中的适当值设置为零来强制进行修剪。我还提供了一个使用美国风格数字参数的构造函数。由于我在书籍代码中经常使用这些,因此我最好让它变得容易……
以下是比较操作,这是一个我仅委托给底层对象的示例行为。
class MfDate...
public boolean after (MfDate arg) { return getTime().after(arg.getTime()); } public boolean before (MfDate arg) { return getTime().before(arg.getTime()); } public int compareTo(Object arg) { MfDate other = (MfDate) arg; return getTime().compareTo(other.getTime()); } public boolean equals(Object arg) { if (! (arg instanceof MfDate)) return false; MfDate other = (MfDate) arg; return (_base.equals(other._base)); } public Date getTime() { return _base.getTime(); }
包装器还允许我添加我认为方便但基本类中没有的行为。
class MfDate...
public MfDate addDays(int arg) { return new MfDate(new GregorianCalendar(getYear(), getMonth(), getDayOfMonth() + arg)); } public MfDate minusDays(int arg) { return addDays(-arg); }