在整个交易过程中始终使用 "now" 的值
Consistently using the value of "now" throughout the transaction
我正在寻找在整个交易过程中使用一致的当前日期和时间值的指南。
我所说的事务泛指应用程序服务方法,此类方法通常执行单个 SQL 事务,至少在我的应用程序中是这样。
环境语境
this question 的答案中描述的一种方法是将当前日期置于环境上下文中,例如DateTimeProvider
,并在所有地方使用它而不是 DateTime.UtcNow
。
然而,这种方法的目的只是使设计可单元测试,而我也想防止因不必要的多次查询 DateTime.UtcNow
而导致的错误,例如:
// In an entity constructor:
this.CreatedAt = DateTime.UtcNow;
this.ModifiedAt = DateTime.UtcNow;
此代码创建的实体的创建日期和修改日期略有不同,而人们希望这些属性在实体创建后立即相等。
此外,环境上下文很难在 Web 应用程序中正确实现,因此我提出了另一种方法:
方法注入 + DeterministicTimeProvider
DeterministicTimeProvider
class 被注册为 "instance per lifetime scope" AKA "instance per HTTP request in a web app" 依赖项。
- 它通过构造函数注入到应用程序服务中,并传递到实体的构造函数和方法中。
- 到处使用
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;
}
这是一个好方法吗?缺点是什么?有更好的选择吗?
代码看起来很合理。
缺点 - 对象的生命周期很可能会由 DI 容器控制,因此提供者的用户无法确保它始终被正确配置(每次调用而不是任何更长的生命周期,如 app/singleton).
如果您有代表 "transaction" 的类型,最好将 "Started" 时间放在那里。
很抱歉诉诸权威 的逻辑谬误,但这很有趣:
约翰·卡马克曾经说过:
There are four principle inputs to a game: keystrokes, mouse moves, network packets, and time. (If you don't consider time an input value, think about it until you do -- it is an important concept)"
来源:John Carmack's .plan posts from 1998 (scribd)
(我一直觉得这句话很有趣,因为如果某件事对你来说不对,你应该认真思考直到它看起来对,这只是一个建议主要极客会说。)
所以,这里有一个想法:将时间作为输入。它可能不包含在xml构成网络服务请求,(无论如何你都不希望它,)但是在你将 xml 转换为实际请求对象的处理程序中,获取当前时间并将其作为你的请求对象的一部分.
因此,由于请求对象在处理交易的过程中在您的系统中传递,因此始终可以在请求[=]中找到被视为"the current time"的时间 35=]。所以,不再是"the current time",而是请求时间。 (完全相同或非常接近相同的事实完全无关紧要。)
这样,测试也变得更加容易:您不必模拟时间提供程序接口,时间始终在输入参数中。
此外,通过这种方式,其他有趣的事情也成为可能,例如在与 实际 当前时刻完全无关的时刻追溯应用服务请求时间。想想可能性。 (带彩虹的鲍勃方裤的照片放在这里。)
嗯..这对 CodeReview.SE than for Whosebug 来说是一个更好的问题,但是当然 - 我会咬人的。
Is this a good approach?
如果使用得当,在你描述的场景中,这种做法是合理的。它实现了两个既定目标:
使您的代码更易于测试。这是我称之为 "Mock the Clock" 的常见模式,在许多设计良好的应用程序中都能找到。
将时间锁定为单个值。这种情况不太常见,但您的代码确实实现了该目标。
What are the disadvantages?
由于您正在为每个请求创建另一个新对象,它会为垃圾收集器创建少量的额外内存使用和额外工作。这有点有争议,因为这通常适用于所有具有按请求生命周期的对象,包括控制器。
在您读取时钟之前,会添加一小部分时间,这是由于在加载对象和执行延迟加载时所做的额外工作造成的。虽然它可以忽略不计 - 可能在几毫秒的数量级。
由于值被锁定,您(或使用您的代码的其他开发人员)始终存在可能引入细微错误的风险,因为您忘记了该值在下一次请求之前不会更改.您可能会考虑不同的命名约定。例如,将其称为 "requestRecievedTime" 或类似名称,而不是 "now"。
与上一项类似,您的提供程序也存在加载错误生命周期的风险。您可能会在新项目中使用它而忘记设置实例化,将其作为单例加载。然后为 all 请求锁定这些值。要强制执行此操作,您无能为力,因此请务必对其进行评论。 <summary>
标签是个好地方。
您可能会发现在无法进行构造函数注入的情况下需要当前时间 - 例如静态方法。您要么必须重构以使用实例方法,要么必须将时间或时间提供程序作为参数传递给静态方法。
Are there better alternatives?
是的,参见 。
您还可以考虑 Noda Time,它通过 IClock
接口以及 SystemClock
和 FakeClock
实现内置了类似的概念。但是,这两种实现都设计为单例。它们有助于测试,但无法实现将每个请求的时间锁定为单个值的第二个目标。不过,您始终可以编写一个执行此操作的实现。
这不是可以通过实时时钟和查询或测试来回答的问题。开发人员可能已经想出了一些到达底层库调用的晦涩方法...
所以不要那样做。依赖注入也不会在这里拯救你;问题是您想要在 'session.'
开始时使用标准时间模式
在我看来,根本问题是你在表达一个想法,并为此寻找一种机制。正确的机制是给它命名,并在名称中说出你的意思,然后只设置一次。 readonly
是在构造函数中仅设置一次的好方法,并让编译器和运行时强制执行您 的意思,即仅设置一次。
// In an entity constructor:
this.CreatedAt = DateTime.UtcNow;
我正在寻找在整个交易过程中使用一致的当前日期和时间值的指南。
我所说的事务泛指应用程序服务方法,此类方法通常执行单个 SQL 事务,至少在我的应用程序中是这样。
环境语境
this question 的答案中描述的一种方法是将当前日期置于环境上下文中,例如DateTimeProvider
,并在所有地方使用它而不是 DateTime.UtcNow
。
然而,这种方法的目的只是使设计可单元测试,而我也想防止因不必要的多次查询 DateTime.UtcNow
而导致的错误,例如:
// In an entity constructor:
this.CreatedAt = DateTime.UtcNow;
this.ModifiedAt = DateTime.UtcNow;
此代码创建的实体的创建日期和修改日期略有不同,而人们希望这些属性在实体创建后立即相等。
此外,环境上下文很难在 Web 应用程序中正确实现,因此我提出了另一种方法:
方法注入 + DeterministicTimeProvider
DeterministicTimeProvider
class 被注册为 "instance per lifetime scope" AKA "instance per HTTP request in a web app" 依赖项。- 它通过构造函数注入到应用程序服务中,并传递到实体的构造函数和方法中。
- 到处使用
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;
}
这是一个好方法吗?缺点是什么?有更好的选择吗?
代码看起来很合理。
缺点 - 对象的生命周期很可能会由 DI 容器控制,因此提供者的用户无法确保它始终被正确配置(每次调用而不是任何更长的生命周期,如 app/singleton).
如果您有代表 "transaction" 的类型,最好将 "Started" 时间放在那里。
很抱歉诉诸权威 的逻辑谬误,但这很有趣:
约翰·卡马克曾经说过:
There are four principle inputs to a game: keystrokes, mouse moves, network packets, and time. (If you don't consider time an input value, think about it until you do -- it is an important concept)"
来源:John Carmack's .plan posts from 1998 (scribd)
(我一直觉得这句话很有趣,因为如果某件事对你来说不对,你应该认真思考直到它看起来对,这只是一个建议主要极客会说。)
所以,这里有一个想法:将时间作为输入。它可能不包含在xml构成网络服务请求,(无论如何你都不希望它,)但是在你将 xml 转换为实际请求对象的处理程序中,获取当前时间并将其作为你的请求对象的一部分.
因此,由于请求对象在处理交易的过程中在您的系统中传递,因此始终可以在请求[=]中找到被视为"the current time"的时间 35=]。所以,不再是"the current time",而是请求时间。 (完全相同或非常接近相同的事实完全无关紧要。)
这样,测试也变得更加容易:您不必模拟时间提供程序接口,时间始终在输入参数中。
此外,通过这种方式,其他有趣的事情也成为可能,例如在与 实际 当前时刻完全无关的时刻追溯应用服务请求时间。想想可能性。 (带彩虹的鲍勃方裤的照片放在这里。)
嗯..这对 CodeReview.SE than for Whosebug 来说是一个更好的问题,但是当然 - 我会咬人的。
Is this a good approach?
如果使用得当,在你描述的场景中,这种做法是合理的。它实现了两个既定目标:
使您的代码更易于测试。这是我称之为 "Mock the Clock" 的常见模式,在许多设计良好的应用程序中都能找到。
将时间锁定为单个值。这种情况不太常见,但您的代码确实实现了该目标。
What are the disadvantages?
由于您正在为每个请求创建另一个新对象,它会为垃圾收集器创建少量的额外内存使用和额外工作。这有点有争议,因为这通常适用于所有具有按请求生命周期的对象,包括控制器。
在您读取时钟之前,会添加一小部分时间,这是由于在加载对象和执行延迟加载时所做的额外工作造成的。虽然它可以忽略不计 - 可能在几毫秒的数量级。
由于值被锁定,您(或使用您的代码的其他开发人员)始终存在可能引入细微错误的风险,因为您忘记了该值在下一次请求之前不会更改.您可能会考虑不同的命名约定。例如,将其称为 "requestRecievedTime" 或类似名称,而不是 "now"。
与上一项类似,您的提供程序也存在加载错误生命周期的风险。您可能会在新项目中使用它而忘记设置实例化,将其作为单例加载。然后为 all 请求锁定这些值。要强制执行此操作,您无能为力,因此请务必对其进行评论。
<summary>
标签是个好地方。您可能会发现在无法进行构造函数注入的情况下需要当前时间 - 例如静态方法。您要么必须重构以使用实例方法,要么必须将时间或时间提供程序作为参数传递给静态方法。
Are there better alternatives?
是的,参见
您还可以考虑 Noda Time,它通过 IClock
接口以及 SystemClock
和 FakeClock
实现内置了类似的概念。但是,这两种实现都设计为单例。它们有助于测试,但无法实现将每个请求的时间锁定为单个值的第二个目标。不过,您始终可以编写一个执行此操作的实现。
这不是可以通过实时时钟和查询或测试来回答的问题。开发人员可能已经想出了一些到达底层库调用的晦涩方法...
所以不要那样做。依赖注入也不会在这里拯救你;问题是您想要在 'session.'
开始时使用标准时间模式在我看来,根本问题是你在表达一个想法,并为此寻找一种机制。正确的机制是给它命名,并在名称中说出你的意思,然后只设置一次。 readonly
是在构造函数中仅设置一次的好方法,并让编译器和运行时强制执行您 的意思,即仅设置一次。
// In an entity constructor:
this.CreatedAt = DateTime.UtcNow;