始终如一地使用"现在"的价值整个交易

时间:2015-09-24 00:50:54

标签: web-services datetime time transactions

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

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

环境背景

this question的答案中描述的一种方法是将当前日期置于环境上下文中,例如DateTimeProvider,并在任何地方使用{而不是DateTime.UtcNow

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

// In an entity constructor:
this.CreatedAt = DateTime.UtcNow;
this.ModifiedAt = DateTime.UtcNow;

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

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

方法注入+ DeterministicTimeProvider

  • DeterministicTimeProvider类在每个生命周期范围内注册为"实例" AKA" Web应用程序中每个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;
}

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

4 个答案:

答案 0 :(得分:8)

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

约翰卡马克曾说过:

  

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

来源:John Carmack's .plan posts from 1998 (scribd)

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

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

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

这样,测试也变得更加容易:您不必模拟时间提供者界面,时间总是在输入参数中。

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

答案 1 :(得分:4)

嗯..这对于CodeReview.SE来说比对StackOverflow感觉更好,但确定 - 我会咬人。

  

这是一个好方法吗?

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

  1. 使您的代码更易于测试。这是我称之为#34;模拟时钟&#34;的常见模式,可以在许多精心设计的应用程序中找到。

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

  3.   

    有哪些缺点?

    1. 由于您要为每个请求创建另一个新对象,因此它将为垃圾收集器创建一些额外的内存使用量和额外的工作量。这有点没有实际意义,因为这通常是每个请求生命周期的所有对象,包括控制器。

    2. 在您从时钟读取数据之前,只需要一小部分时间,这是由于在加载对象和进行延迟加载时所做的额外工作。但它可以忽略不计 - 可能只有几毫秒。

    3. 由于该值已被锁定,因此您(或使用您的代码的其他开发人员)可能会因为忘记该值不会发生变化而引入一个微妙的错误。下一个请求。您可以考虑使用不同的命名约定。例如,代替&#34;现在&#34;,将其称为&#34; requestRecievedTime&#34;或类似的东西。

    4. 与上一项相似,您的提供程序可能会加载错误的生命周期。您可以在新项目中使用它,忘记设置实例,将其加载为单例。然后,为所有请求锁定值。您无法执行此操作,因此请务必对其进行评论。 <summary>标记是个好地方。

    5. 您可能会发现在构造函数注入不可能的情况下需要当前时间 - 例如静态方法。您必须重构以使用实例方法,或者必须将时间或时间提供程序作为参数传递给静态方法。

    6.   

      有更好的选择吗?

      是的,请参阅Mike's answer

      您也可以通过IClock界面以及SystemClockFakeClock实施来考虑内置类似概念的Noda Time。但是,这两种实现都被设计为单例。他们帮助进行测试,但他们没有实现第二个目标,即将时间锁定为每个请求的单个值。你总是可以编写一个实现它的实现。

答案 2 :(得分:2)

代码看起来很合理。

缺点 - 对象的生命周期最有可能由DI容器控制,因此提供者的用户无法确保始终正确配置(每次调用,而不是像app / singleton那样的生命周期)

如果您有类型代表&#34;交易&#34;放入&#34;开始&#34;可能会更好。而是在那里。

答案 3 :(得分:0)

这不是可以通过实时时钟和查询或通过测试来回答的问题。开发人员可能已经找到了一些模糊的方法来访问底层库调用...

所以不要这样做。依赖注入也不会在这里拯救你;问题是你想要在会议开始时采用标准的时间模式。&#39;

在我看来,根本问题在于你表达了一个想法,并寻找一种机制。正确的机制是命名它,并在名称中说出你的意思,然后只设置一次。 INSERT INTO members (location, story) SELECT '$location', '$story' FROM table_name WHERE username='$user' ON DUPLICATE KEY UPDATE hash = '$password', location='$location', story='$story' 是处理构造函数中只设置一次的好方法,并允许编译器和运行时强制执行意味着的内容,即只设置一次。

readonly