O lokalności LocalDateTime

Salvador Dali - Trwałość pamięci

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.


Autor Tomek Fijałkowski

Tomek jest starszym programistą w Allegro. Jest pasjonatem zwinnych metodyk i DDD. Od kilku lat zajmuje się rozwojem systemu opartego o mikrousługi, w ostatnim czasie koncentrując się na rozwiązaniach pozwalających zbudować spójne narzędzie admińskie obsługi klienta w świecie ponad 700 mikrousług.

6 thoughts on “O lokalności LocalDateTime

  1. 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

    1. 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.

Leave a Reply