Последовательно используя значение "сейчас" на протяжении всей транзакции
Я ищу рекомендации по использованию согласованного значения текущей даты и времени в течение транзакции.
Под транзакцией я имею в виду метод обслуживания приложения, такие методы обычно выполняют одну транзакцию SQL, по крайней мере, в моих приложениях.
Окружающий контекст
Один из подходов, описанных в ответах на этот вопрос, заключается в том, чтобы поместить текущую дату в окружающий контекст, например, DateTimeProvider
и использовать это вместо DateTime.UtcNow
везде.
Однако цель этого подхода состоит только в том, чтобы сделать тестирование модуля модульным, в то время как я также хочу предотвратить ошибки, вызванные ненужными многократными запросами в DateTime.UtcNow
Примером которого является это:
// In an entity constructor:
this.CreatedAt = DateTime.UtcNow;
this.ModifiedAt = DateTime.UtcNow;
Этот код создает объект с немного отличающимися датами создания и изменения, тогда как можно ожидать, что эти свойства будут равны сразу после создания объекта.
Кроме того, окружающий контекст трудно реализовать правильно в веб-приложении, поэтому я предложил альтернативный подход:
Инъекция метода + 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;
}
Это хороший подход? Каковы недостатки? Есть ли лучшие альтернативы?
4 ответа
Хм... это похоже на лучший вопрос для CodeReview.SE, чем для Stackru, но уверен - я укушу.
Это хороший подход?
При правильном использовании в описанном вами сценарии такой подход является разумным. Это достигает двух заявленных целей:
Делаем ваш код более тестируемым. Это распространенный шаблон, который я называю "Mock the Clock", и он встречается во многих хорошо разработанных приложениях.
Блокировка времени на одно значение. Это не так часто, но ваш код достигает этой цели.
Каковы недостатки?
Так как вы создаете еще один новый объект для каждого запроса, он создает небольшое количество дополнительного использования памяти и дополнительной работы для сборщика мусора. Это в какой-то степени спорный вопрос, поскольку обычно так обстоит дело со всеми объектами с временем жизни каждого запроса, включая контроллеры.
До считывания с часов добавляется крошечная доля времени, вызванная дополнительной работой, выполняемой при загрузке объекта и отложенной загрузкой. Это незначительно, хотя - вероятно, порядка нескольких миллисекунд.
Поскольку значение заблокировано, всегда существует риск того, что вы (или другой разработчик, который использует ваш код) могут внести небольшую ошибку, забыв, что значение не изменится до следующего запроса. Вы могли бы рассмотреть другое соглашение об именах. Например, вместо "сейчас", назовите его "requestReceveTime" или что-то в этом роде.
Как и в предыдущем пункте, существует также риск того, что ваш поставщик может быть загружен с неправильным жизненным циклом. Вы можете использовать его в новом проекте и забыть установить экземпляр, загружая его как одноэлементный. Затем значения блокируются для всех запросов. Вы ничего не можете сделать, чтобы обеспечить соблюдение этого, поэтому обязательно прокомментируйте это хорошо.
<summary>
тег это хорошее место.Вы можете обнаружить, что вам нужно текущее время в сценарии, где внедрение конструктора невозможно, например, в статическом методе. Вам придется либо выполнить рефакторинг для использования методов экземпляра, либо вам придется передать либо время, либо поставщика времени в качестве параметра в статический метод.
Есть ли лучшие альтернативы?
Да, смотрите ответ Майка.
Вы также можете рассмотреть Noda Time, который имеет похожую концепцию, через IClock
интерфейс, а SystemClock
а также FakeClock
Реализации. Тем не менее, обе эти реализации предназначены для одиночных игр. Они помогают с тестированием, но не достигают вашей второй цели - сократить время до одного значения на запрос. Вы всегда можете написать реализацию, которая делает это, хотя.
Извините за логическую ошибку обращения к власти здесь, но это довольно интересно:
Джон Кармак однажды сказал:
В игре есть четыре основных входа: нажатия клавиш, движения мыши, сетевые пакеты и время. (Если вы не учитываете время как входное значение, думайте об этом, пока не примете - это важная концепция)"
Источник: сообщения Джона Кармака.plan с 1998 года (scribd)
(Я всегда находил эту цитату очень забавной, потому что предположение, что если что-то не кажется вам правильным, вы должны думать об этом очень тяжело, пока оно не покажется правильным, - это то, что скажет только один крупный гик.)
Итак, вот идея: рассмотрите время как вход. Вероятно, он не включен в xml, который составляет запрос веб-службы (в любом случае, вы бы этого не хотели), но в обработчике, в котором вы преобразуете xml в фактический объект запроса, получаете текущее время и делаете его частью вашего объекта запроса.
Таким образом, поскольку объект запроса передается по вашей системе в ходе обработки транзакции, время, которое следует считать "текущим временем", всегда можно найти в запросе. Таким образом, это уже не "текущее время", а время запроса. (Тот факт, что это будет одно и то же или очень близкое к одному и тому же, совершенно не имеет значения.)
Таким образом, тестирование также становится еще проще: вам не нужно издеваться над интерфейсом провайдера времени, время всегда находится во входных параметрах.
Кроме того, таким образом становятся возможными другие забавные вещи, например, запросы на обслуживание, которые должны применяться задним числом, в момент времени, который совершенно не связан с фактическим текущим моментом времени. Подумайте о возможностях. (Здесь изображена квадратная штанга Боба с радугой.)
Код выглядит разумно.
Недостаток - вероятнее всего, время жизни объекта будет контролироваться контейнером DI, и, следовательно, пользователь провайдера не может быть уверен, что он всегда настроен правильно (для каждого вызова, а не для более длительного времени жизни, как app/singleton).
Если у вас есть тип, представляющий "транзакцию", может быть лучше вместо этого указать время "Начало".
Это не то, на что можно ответить с помощью часов реального времени и запроса, или путем тестирования. Разработчик, возможно, нашел какой-то непонятный способ достичь основного вызова библиотеки...
Так что не делай этого. Инъекция зависимости также не спасет вас здесь; проблема в том, что вам нужен стандартный шаблон времени в начале сеанса.
На мой взгляд, фундаментальная проблема заключается в том, что вы выражаете идею и ищете механизм для этого. Правильный механизм - назвать его и сказать, что вы имеете в виду в названии, а затем установить его только один раз. readonly
это хороший способ обработать установку этого параметра только один раз в конструкторе, и он позволяет компилятору и среде исполнения реализовать то, что вы имеете в виду, а именно то, что оно устанавливается только один раз.
// In an entity constructor:
this.CreatedAt = DateTime.UtcNow;