在整个交易过程中始终使用"现在"的值

Geb*_*ebb 17 time datetime web-services transactions

我正在寻找在整个交易中使用当前日期和时间的一致值的指导原则.

通过事务我松散地表示应用程序服务方法,这种方法通常执行单个SQL事务,至少在我的应用程序中.

环境背景

这个问题的答案中描述的一种方法是将当前日期放在环境上下文中,例如DateTimeProvider,并使用它来代替DateTime.UtcNow任何地方.

然而,这种方法的目的只是为了使设计单元可测试,而我还想防止由不必要的多次查询引起的错误DateTime.UtcNow,其中一个例子如下:

// In an entity constructor:
this.CreatedAt = DateTime.UtcNow;
this.ModifiedAt = DateTime.UtcNow;
Run Code Online (Sandbox Code Playgroud)

此代码创建的实体具有稍微不同的创建和修改日期,而人们希望这些属性在创建实体后立即相等.

此外,环境上下文很难在Web应用程序中正确实现,所以我想出了另一种方法:

方法注入+ DeterministicTimeProvider

  • 所述DeterministicTimeProvider类被登记为"每寿命实例范围" AKA依赖性"每HTTP请求在网络应用程序的实例".
  • 它是构造函数注入到应用程序服务并传递给实体的构造函数和方法.
  • 使用该IDateTimeProvider.UtcNow方法代替通常DateTime.UtcNow/ DateTimeOffset.UtcNow到处获取当前日期和时间.

这是实施:

/// <summary>
/// Provides the current date and time.
/// The provided value is fixed when it is requested for the first time.
/// </summary>
public class DeterministicTimeProvider: IDateTimeProvider
{
    private readonly Lazy<DateTimeOffset> _lazyUtcNow =
        new Lazy<DateTimeOffset>(() => DateTimeOffset.UtcNow);

    /// <summary>
    /// Gets the current date and time in the UTC time zone.
    /// </summary>
    public DateTimeOffset UtcNow => _lazyUtcNow.Value;
}
Run Code Online (Sandbox Code Playgroud)

这是一个好方法吗?有什么缺点?还有更好的选择吗?

Mik*_*kis 8

对于在这里诉诸权威的逻辑谬误感到抱歉,但这很有趣:

约翰卡马克曾说过:

游戏有四个主要输入:击键,鼠标移动,网络数据包和时间.(如果你不把时间视为输入值,那么在你做之前要考虑它 - 这是一个重要的概念)"

资料来源:1998年John Carmack的.plan帖子(scribd)

(我总是觉得这句话非常有趣,因为有人认为,如果某些东西对你来说不合适,你应该认为它真的很难,直到它看起来是正确的,这只是一个主要的极客会说的.)

所以,这是一个想法:将时间视为输入.它可能不包含在构成Web服务请求的xml中(您不会想要它),但是在将xml转换为实际请求对象的处理程序中,获取当前时间并使其成为一部分您的请求对象.

因此,当在处理事务的过程中在您的系统中传递请求对象时,始终可以在请求中找到被视为"当前时间"的时间.所以,它不再是"当前时间",而是请求时间.(事实上​​,它将是同一个,或者非常接近同一个,这完全是无关紧要的.)

这样,测试也变得更加容易:您不必模拟时间提供程序接口,时间总是在输入参数中.

而且,通过这种方式,其他有趣的事情变得可能,例如,服务请求被追溯地应用在与实际当前时刻完全无关的时刻.想想可能性.(bob squarepants-with-a rainbow的图片就在这里.)

  • 我不确定我的措辞是否清楚,但我没有在这里发布这个简单的替代方案; 我发布它作为最有意义的解决方案*到目前为止*.*当前时间*是我经常看到被程序员误用的概念.大多数情况下,程序员*认为他们需要当前时间,他们实际需要的是正在执行的操作时间.应该在尽可能少的地方隔离操作时间应该是实际实时的事实.模拟时钟的需要通常表明时间的误用. (2认同)

Mat*_*int 4

嗯.. 对于CodeReview.SE来说,这感觉比对于StackOverflow来说是一个更好的问题,但是当然 - 我会咬住。

这是一个好方法吗?

如果使用正确,在您描述的场景中,这种方法是合理的。它实现了两个既定目标:

  1. 使您的代码更具可测试性。这是一种常见的模式,我称之为“模拟时钟”,并且可以在许多设计良好的应用程序中找到。

  2. 将时间锁定为单个值。这种情况不太常见,但您的代码确实实现了该目标。

有什么缺点?

  1. 由于您要为每个请求创建另一个新对象,因此它将产生少量的额外内存使用量以及垃圾收集器的额外工作。这在某种程度上是一个有争议的问题,因为这通常是所有具有每个请求生命周期的对象(包括控制器)的处理方式。

  2. 在从时钟读取读数之前会添加一小部分时间,这是由于加载对象和延迟加载时所做的额外工作造成的。不过它可以忽略不计——可能只有几毫秒。

  3. 由于该值被锁定,因此始终存在这样的风险:您(或使用您的代码的其他开发人员)可能会忘记该值在下一个请求之前不会更改,从而引入微妙的错误。您可以考虑使用不同的命名约定。例如,将其称为“requestRecievedTime”或类似名称,而不是“now”。

  4. 与上一项类似,您的提供程序也存在加载错误生命周期的风险。您可能会在新项目中使用它,但忘记设置实例,将其作为单例加载。然后,所有请求的值都会被锁定。您无法执行太多操作来强制执行此操作,因此请务必对其进行注释。标签<summary>是个好地方。

  5. 您可能会发现在无法进行构造函数注入的场景(例如静态方法)中需要当前时间。您要么必须重构才能使用实例方法,要么必须将时间或时间提供者作为参数传递到静态方法中。

有更好的选择吗?

是的,请参阅迈克的回答

您还可以考虑Noda TimeIClock ,它通过接口和SystemClock实现内置了类似的概念FakeClock。然而,这两种实现都被设计为单例。它们有助于测试,但无法实现将每个请求的时间锁定为单个值的第二个目标。不过,您始终可以编写一个实现来实现这一点。