W kłopoty wpędza cię nie to, czego nie wiesz, ale to, co wiesz, a co nie jest prawdą.
— Will RogersPodstawy 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ę choćby w bibliotece standardowej Javy. Weźmy na tapetę klasę LocalDateTime. Co to za klasa? Nazwa sugeruje, iż 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, iż 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, iż LocalDateTime ma kilka wspólnego z lokalnością. Z resztą ten sam problem dotyczy LocalDate. jeżeli zobaczysz wyrażenie
ZonedDateTime now = LocalDateTime.now().atZone(ZoneId.of("UTC")))to duże szanse, iż to bug. Kilka razy już naprawiałem takie błędy. Nie są one łatwe do wykrycia, jeżeli 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, ale 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ż iż 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 = LocalDateTimeZapomniałabym, jeżeli umiesz wymyślić lepszą nazwę, koniecznie do mnie napisz.
Przekaz na dziś
Na koniec moje ulubione rozwiązanie: nie używaj LocalDateTime, jeżeli to tylko możliwe.