W kłopoty wpędza cię nie to, czego nie wiesz, lecz to, co wiesz, a co nie jest prawdą.
— Will Rogers
Podstawy których nie znają seniorzy
Wszyscy wiemy jak ważnym zagadnieniem jest dobre nazewnictwo zmiennych, funkcji, klas i wszystkiego czym operujemy. Jednym z popularniejszych technik refaktoryzacji jest zmiana nazwy. Programiści spędzają sporo czasu nad wymyślaniem nazw. Można by więc oczekiwać, aby każda nazwa miała sens i była poprawna, a w sytuacji gdy tak nie jest, abyśmy mogli zmienić tą nazwę.
Niestety, życie jest okrutne! Smród pojawia się nawet w bibliotece standardowej Javy. Weźmy na tapetę klasę LocalDateTime
. Co to za klasa? Nazwa sugeruje, że przechowuje lokalną datę. Takie też odpowiedzi słyszę na rozmowach rekrutacyjnych (o ile ktoś kojarzy api java.time
).
Lokalność tylko z nazwy
Przeprowadźmy więc mały eksperyment. W moim komputerze mam ustawioną polską strefę czasową, co oznacza, że poniższa asercja jest poprawna.
assert ZoneId.systemDefault().equals(ZoneId.of("Europe/Warsaw"))
Jeśli LocalDateTime
przechowuje datę w lokalnej strefie czasowej, to aby uzyskać aktualną datę w innej strefie, powinienem móc wykonać kod:
ZonedDateTime now = LocalDateTime.now().atZone(ZoneId.of("UTC")))
a poniższa asercja powinna przejść
assert now.equals(ZonedDateTime.now(Clock.system(ZoneId.of("UTC"))))
Asercja jednak nie przechodzi. Jest to spowodowane tym, że LocalDateTime
ma niewiele wspólnego z lokalnością. Z resztą ten sam problem dotyczy LocalDate
. Jeśli zobaczysz wyrażenie
ZonedDateTime now = LocalDateTime.now().atZone(ZoneId.of("UTC")))
to duże szanse, że to bug. Kilka razy już naprawiałem takie błędy. Nie są one łatwe do wykrycia, jeśli tworzenie instancji LocalDateTime
i zamiana na ZonedDateTime są od siebie odległe, np. znajdują się w różnych plikach.
Właściwie metoda atZone
klasy LocalDateTime
ma sens tylko gdy znamy kontekst tworzenia instacji – wiemy w jakiej strefie czasowej została utworzona instancja LocalDateTime
.
OK, to co tu się dzieje? Metoda LocalDateTime.now()
zwraca aktualną datę systemową a następnie jest do niej dostawiana wybrana strefa. W naszym przypadku jest to UTC. Jak się okazuje, nie ma to nic wspólnego z teraz (now). Wynikiem jest „teraz” przesunięte o różnice stref czasowych między strefą czasową systemową a UTC.
To zachowanie nie jest zaskakujące dla osób czytających dokumentację.
A date-time without a time-zone in the ISO-8601 calendar system, such as
2007-12-03T10:15:30
.
Po co jednak czytać dokumentację skoro nazwa wszystko tłumaczy? Wskazówką jest powyższe motto. Jak powiedział Will Rogers, w kłopoty wpędza cię nie to, czego nie wiesz, lecz to, co wiesz, a co nie jest prawdą.
Drogi czytelniku, jestem Ci jeszcze winien przedstawić poprawny przykład kodu. Rozumiejąc już jak działa LocalDateTime
, wyrażenie
ZonedDateTime now = LocalDateTime.now().atZone(ZoneId.of("UTC"))
powinno być zastąpione przez
ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC"))
Jak żyć?
Wiemy już że LocalDateTime
to słaba, wprowadzająca w błąd, nazwa. Tak więc jak żyć?
Może lepszą nazwą byłoby ZonelessDateTime
w analogii do ZonedDateTime
? Ewentualnie podobnie NoZoneDateTime
lub NotZonedDateTime
. Nazwa ta ma jeden mankament. Generalnie nazwa klasy powinna mówić za co obiekt odpowiada a nie za co nie odpowiada. To może po prostu DateTime
?
Jeśli umiesz wymyślić lepszą nazwę (lub pasuje Ci jedna z moich propozycji) i używasz np. kotlina to możesz stosować aliasy typów
typealias ZonelessDateTime = LocalDateTime
Zapomniałabym, jeśli umiesz wymyślić lepszą nazwę, koniecznie do mnie napisz.
Przekaz na dziś
Na koniec moje ulubione rozwiązanie: nie używaj LocalDateTime
, jeśli to tylko możliwe.
W dokumentacji Pythona używa się terminu „naive” na czas ignorujący strefę czasową oraz „aware” na czas ją uwzględniający. Tak więc lepszą nazwą dla LocalDateTime mogłoby być NaiveDateTime
Dzięki za ten komentarz, NaiveDateTime jest ciekawą propozycją.
Jeżeli nie LocalDateTime to w takim razie co innego? Instant?
To zależy czego potrzebujesz. Jeśli wystarczy Ci znacznik czasu to Instatn jest w sam raz. Jeśli potrzebujesz jakiejś logiki na datach, np. warunek czy upłynął miesiąc, to proponuję ZonedDateTime lub OffsetDateTime. Wszystko zależy od konkretnej potrzeby.
Wow. Dzięki wielkie za ten wpis ?