Постоянство памяти Сальвадора Дали.

Есть несколько вещей, которые я узнал, выйдя из большого проекта, и прежде чем я объединил их в какой-то публикации Agile Development Best Practices, я решил сначала написать их в небольших блогах, чтобы иметь возможность делиться ими раньше и получать отзывы. .

В то время, когда я начал писать эту серию статей, постоянно возникала проблема с DateTime. Работа с датами и временем может оказаться на удивление крепким орешком. Это не так хорошо известно, как должно быть.

Различные значения настоящего

Вы могли бы подумать, что концепция «сейчас» — «дата и время, которые вы видите, глядя на часы» — довольно проста. Многие устройства сообщат вам текущую дату и время, и вы можете подумать, что это так. Но когда вы начинаете писать программы, использующие текущую дату и время, все быстро усложняется. Или, что еще хуже, медленно…

К тому времени, когда вы поймете, что у вас есть проблема, вы, возможно, напишете много строк кода и в несколько раз больше строк тестов (более крупные проекты могут заканчиваться тысячами). Необходимость рефакторинга большого количества кода, повторных преобразований, возврата тестов к зеленому цвету может стать пугающим процессом. Поэтому лучший способ подготовиться к этому — убедиться, что вы абстрагируете свою логику DateTime.

Но какие проблемы, спросите вы? Мы не будем вдаваться в них слишком подробно, поскольку здесь речь идет о том, как быть гибким, когда вы сталкиваетесь с проблемой, о которой вы не думали заранее, но давайте сосредоточимся на наиболее важных концепциях.

«Использование Agile с разработкой DateTime может быть похоже на медленно готовящуюся лягушку. За исключением того, что вопреки распространенному мнению, лягушки действительно выпрыгивают из кастрюли, когда становится слишком жарко».

Время на вашей стороне

Это та часть, где я пытаюсь убедить вас, что у вас могут быть проблемы сейчас или в будущем. Если вы уже это знаете и у вас мало времени, то смело переходите к следующему разделу.

Все еще здесь? Хорошо, тогда поехали.

Допустим, я пишу программу, возможно, для больницы или астрологии, которая записывает точное время и дату моего рождения, и давайте работать с иллюзией, что я уверен, что никто никогда не захочет, чтобы я что-то делал. еще с этими данными. Просто, верно? Допустим, я использовал .NET и сохранил объект DateTime и сохранил его в базе данных SQL. Когда я загружу его снова, я просто получу значение, которое я туда вставил, и все. Ну, то есть, если база данных SQL работает на машине с теми же региональными настройками, что и у клиента или сервера, на котором работает моя машина, есть большая вероятность, что значение будет аналогичным, верно? Но если бы у меня был пользователь из Великобритании, и он прочитал бы мое значение, для него это было бы на час раньше.

Это проблема? Зависит от того, что вы хотите сделать, очевидно, но нетрудно представить сценарий, который мог бы его создать. Что, если бы я просто родился в 00:15? По британскому времени это будет 23:15 предыдущего дня. Значит ли это, что красивое маркетинговое письмо должно поздравить меня с днем ​​рождения на день раньше? Хм.

Так что, возможно, если я просто буду делать все для каждой страны, размещать там отдельные серверы, иметь одинаковых клиентов, у меня не будет этой проблемы, верно? Хорошо, но подождите, в некоторых странах есть несколько часовых поясов (вы знаете, небольшие крайние случаи, такие как США, Россия, Китай…). Что ж, хорошо, но я так или иначе нацелился на маленькую страну с одним часовым поясом, и она будет полностью локализована и все такое, так что может пойти не так? Что ж …

Итак, кто-то в больнице нажимает эту симпатичную маленькую кнопку, которая регистрирует, когда ребенок родился в 00:15, и вуаля, вот и все. Правда, это было летом, а потом наступает зимнее время. Ребенок родился в 23:15 предыдущего дня? Нет, очевидно, но что, если бы программист подумал

«Я где-то читал, что если я использую значение DateTime, я всегда должен использовать UTC».

…тогда очень вероятно, да, день рождения малыша, если вы посмотрите на него зимой, будет на день раньше, чем вы ожидали. Потому что, вы знаете, каждый программист учится в школе, что если вы просто используете универсальное скоординированное время, то все проблемы с датой и временем растают, как снег перед солнцем.

А что, если бы у меня была база данных с людьми с датами и временем рождения, и я хотел бы упорядочить их по возрасту, указав сначала самых старых. Если бы все они жили в одном часовом поясе страны, это было бы довольно просто. Но что, если они этого не сделали? Тогда 00:15 — это не то же самое, что 00:15, поэтому мне нужна точка соприкосновения. UTC может быть здесь хорошо, верно? Да, верно, для сортировки это неплохо, не так уж важно, день рождения у этих людей на день раньше, в зависимости от того, смотрю ли я на это зимой или летом. Для сортировки… Конечно, у меня не было бы другого применения для этих дат… И не забывайте о беспорядке с сортировкой данных, когда часы переводятся на летнее время.

Есть также несколько действительно хороших историй о забавных вариантах часового пояса, и, конечно, все знают, что у Германии была небольшая зона земли в Швейцарии, у которой был свой часовой пояс, пока он не закончился в 2013 году, верно? (Я узнал это в реальном проекте). Так что, возможно, если кто-то родился там до этой даты…

«Мудрость приходит с возрастом и опытом, если вам повезет, но узнавать, что вы не все знаете, и готовиться к этому всегда лучше, чем делать ставку на то, что все получится с первого раза».

Так быстро (ну, не в моем случае, это заняло около 10 лет) вы начинаете понимать, что если вы хотите иметь возможность делать со свиданием все, что вы, возможно, захотите, вам нужно иметь гораздо больше информации, чем вы могли бы иметь. думали ранее: вам понадобятся дата и время, когда оно было записано, часовой пояс, в котором оно было записано, и было ли активно летнее время. Итак, если я скажу, что родился в 7:15 сегодня, но все эти годы назад мы использовали немного разные данные о часовом поясе, если мне затем нужно сравнить дату моего рождения с кем-то еще из другого часового пояса… Вы поняли идею.

К счастью, в некоторых случаях можно обойтись короткими путями. Например, допустим, нам нужно отслеживать, когда открывается магазин, и мы хотим опубликовать это на Картах Google. Магазин (один из тех олдскульных, не онлайновых) открывается, скажем, в 07:00, независимо от того, зима сейчас или лето. И неважно, родом я из Австралии или Бразилии, когда я иду в этот магазин, меня обычно интересует только местное время. Кроме того, как разработчик или конечный потребитель, если я знаю, что это местное время, я, вероятно, даже могу перевести его на другие часовые пояса, если это необходимо.

Поэтому я знаю, что время для определения местоположения этой точки на Картах Google будет в самый раз. Я сохраняю его в базе данных только для этого места, и вуаля. За исключением … Я должен убедиться, что мой элемент управления пользовательского интерфейса записывает время в правильном формате, и если он сериализуется в .NET, он сохраняет этот формат, и если он сохраняется, скажем, Entity Framework, чтобы сказать, что база данных SQL сохраняется этот формат, и когда он проходит весь путь назад, он все еще сохраняет этот формат, и это согласуется с получением значения «Сейчас» в бэкэнде, если это необходимо, и …. Другие базы данных тоже могут иметь совершенно другие проблемы  — им может не хватать необходимой точности, например (например, MySQL), и вдруг вы в конечном итоге предпочтете вместо этого хранить значение как двойное, содержащее тики (прочитайте мой следующий блог об отделении логики от состояния для подробнее о том, как с этим бороться).

Итак, после некоторого размышления вы начали изучать DateTimeKind (не указано для победы) и TimeSpan (только для сопоставления со временем), вы узнали, как JSON.NET переводит этот формат в json назад и из, вы узнаете, что некоторые из ваших элементов управления html всегда дают вернуться по местному или UTC, но не без указания и компенсировать это, постепенно вы куда-то движетесь, но вы хотели бы знать все это заранее … .

Итак, что я могу сделать, чтобы защитить свой проект от этого в будущем?

Создайте интерфейс IDateTimeProvider, создайте ICustomDateTime, реализуйте его и используйте неукоснительно. Добавьте любое семантическое изменение, любой тип операции «Сейчас», который вам нужен, в качестве отдельного метода и убедитесь, что вы даете им четкие имена.

Например, при регистрации часов работы магазина у нас есть магазин в фиксированном месте с часами работы относительно местных часов. Поэтому создайте новый метод: GetLocationDateTime (действительный для этого местоположения). Теперь все проще  — пока я могу связать DateTime с местоположением и я могу связать местоположение с правильным часовым поясом и т. д., мне нужно только сохранить нейтральное значение DateTime.

Если я хочу отслеживать, когда пользователь посещал этот магазин с точки зрения магазина, мне нужно использовать ту же функцию. Но если мне нужна точка зрения пользователя (например, чтобы отслеживать, когда я посещал какой магазин), тогда используйте GetUserDateTime (это будет, например, с точки зрения того, где я живу, а не где находится магазин).

Возможно, я создаю InvoiceProposals, тогда лучше всего поместить туда GetInvoiceDateTime для хорошей меры. Еще один вопрос, о котором вы захотите подумать, — это то, должен ли ваш Now быть таким же сейчас во время вашего REST-вызова/сеанса. В некоторых случаях вам может понадобиться это, и в этом случае вы можете создать GetSessionDateTime(). Вы поблагодарите себя позже, даже если в конечном итоге вы поступили правильно и внедрили эту временную метку с надлежащего верхнего уровня.

«Время… абсолютно относительно».

И помните, когда вы совершенно не уверены в том, что вам может понадобиться, рассмотрите возможность реализации этого GetAwesomeDateTime, который сохраняет DateTime, TimeZone и летнее время на момент записи DateTime. У вас должна быть возможность перейти оттуда куда угодно, поскольку вы точно знаете, какое время имелось в виду именно в то время, когда оно имело значение. Любой другой формат открыт для интерпретации!

Еще одно огромное преимущество того, что IDateTimeProvider предоставляет все ваши функции, связанные со временем, заключается в том, что вы становитесь хозяином прямо сейчас. Это необходимо для тестирования. Если вы хотите проверить, может ли ваша бизнес-логика обрабатывать последнюю минуту дня, первый или последний день месяца, последний день февраля, последний день года, переводы на специальные дни в других календарях, что угодно: что может быть приятнее, чем возможность заставить вашу фиктивную структуру решать, с какой датой и временем тестировать, или даже вводить целый диапазон.

Что касается вашего ICustomDateTime и другой логики (скажем, вычисления интервалов, сроков действия и т. д.), то все они, конечно, все еще могут сначала обернуть DateTime .NET, или, возможно, вы сразу же перейдете к NodaTime. Но самое главное здесь то, что теперь вы убедились, что можете переключаться, когда это необходимо, без необходимости выполнять слишком много кода и тестов. И что немаловажно, вы можете выдавать ошибки, когда кто-то делает что-то, чего не должен…

«Надеюсь, мы сэкономим вам немного времени здесь…»

Говоря об этом, когда придет время где-то хранить эти даты и время, не забудьте проверить поставщика вашей базы данных или лучшие практики ORM, когда вы реализуете их для хранения. Здесь тоже есть несколько предостережений  — если вы храните DateTime в SQL, а ваш DateTimeKind — это UTC, когда вы в следующий раз извлечете его обратно в DateTime, он будет иметь DateTimeKind Unspecified. Это может быть довольно… плохо, особенно когда этот DateTime переходит в Json позже (привет, четыре часа разницы во времени), поэтому в этих случаях оберните свои поля в некоторую логику, которая восстанавливает правильный DateTimeKind (например, используйте атрибут).

Вы также можете использовать типы столбцов «Дата» и «Время» отдельно и, например, указать время с точностью до минуты (может быть удобно для целей календаря и т. д.). Вы по-прежнему можете запрашивать их как комбинированное значение DateTime для сортировки и отображать их в свойстве, доступном только для чтения. С такой библиотекой, как MySQL или SQLLite, вы можете захотеть просто сохранить время (или даже дату и время) как двойное, потому что вам может потребоваться больше точности, а не меньше, а их базовые типы могут не иметь нужной вам точности. Если вы просто хотите сохранить время, используйте TimeSpan, который автоматически сопоставляется со столбцом времени, например, в EntityFramework. Если вам посчастливилось использовать NHibernate, есть несколько изящных трюков, которые вы можете проделать с сопоставлениями типов. Иногда кажется, что хлопотам просто нет конца, но пока вы используете правильные шаблоны и интерфейсы, у вас есть достаточно возможностей изменить свою реализацию, чтобы использовать правильные решения без лишнего рефакторинга (рефакторинг в большом проекте может обойтись очень дорого). ).

«Быть ​​или иметь Время…?»

Кроме того, при добавлении функциональности DateTime к объекту обязательно учитывайте подход «HasA», а не «IsA». Например, если ваши объекты будут иметь StartDateTime и (необязательно) EndDateTime, указывающие, как долго этот объект был действителен, создайте объект ValidPeriod и добавьте его к объектам, которые в нем нуждаются (Совет: в EntityFramework HasA лучше всего делать с помощью отдельный объект, который имеет общий первичный ключ и сопоставлен с той же таблицей, поскольку комплексные типы не могут обрабатывать значения, допускающие значение NULL.)

ВЫВОД

Если из всего этого изложения вынести что-то одно, то пусть это будет следующее:

Абстрагируйте свои функции, объекты и свойства DateTime прямо в начале вашего проекта, чтобы их можно было легко изменить при необходимости и должным образом протестировать. Вы поблагодарите себя в будущем и станете гораздо более проворными, когда это будет необходимо.