Mądre zarządzanie czasem nie jest łatwe – w dużej mierze dotyczy się to życia prywatnego ale również ma swoje odniesienie w technologii. Każda operacja, która wplata czas w swoją domenę jest warta zastanowienia – jak, po co, dlaczego i dla kogo to ma służyć? Świat jest duży, istnieją różne strefy czasowe, przesilenia jesienne/wiosenne. To wszystko wpływa na czas i programy, które ten czas implementują. Raz błędnie zaimplementowana logika może ponieść za sobą olbrzymie konsekwencje.
Cześć 🙂
Nie będzie to artykuł, który poprowadzi Cię szczegółowo po dokumentacji API. Chciałbym Ci przedstawić główne idee dotyczące czasu oraz abyś zrozumiał jak używać czasu. Dziwnie to brzmi jak używać czasu, ale taka jest koncepcja. Wiele osób nie zastanawia się nad tym, walą LocalDateTime.now() i wszystko gra bo u mnie działa. Świadomi programiści tak nie robią. Na początku penetrują API, później zastanawiają się jakie mogą być konsekwencje ich wyborów a następnie podejmują decyzje. Najlepiej takie decyzje, które mogą być odwracalne.
W dzisiejszym artykule:
API Dawniej i Dziś
Bez zbędnego gadania – API do zarządzania datą przed Javą 8 było słabe – klasy były mutowalne i niebezpieczne dla środowisk wielowątkowych (DateFormat). Jako, że API zostało zaprojektowane już dawno temu (java.util.Date – JDK 1 / java.util.Calendar – JDK 1.1) i było w użyciu przez wiele lat to jego wady zostały zauważone i podkreślane przez innych. Dzięki temu powstawały rozwiązania poza JDK, które ułatwiały pracę z czasem. Najpopularniejszą biblioteką jaka powstała jest Joda-Time (jeżeli utrzymujesz projekt posiadający wersję Javy niższą niż 8 i potrzebujesz zarządzać czasem, rozważ użycie tej biblioteki).
Tak było kiedyś. Architekci Javy widząc problem i inspirując się API Joda-Time doszli do wniosku, że aktualne rozwiązanie trzeba zaorać. I tak oto powstało nowe API, znajdujące się w JDK w pakiecie java.time. Uwagi projektowe zamieszczone w dokumentacji najlepiej opisują czym kierowali się twórcy projektując nowe API. Z notatek technicznych można wskazać 3 główne cechy nowego API:
- Niemutowalność – to była zmora dawnego API.
- Domain Driven Design – tworzone API ma być oparte na zachowaniach domeny – szczególnie istotne jest odróżnienie przypadków biznesowych dla Daty i Czasu.
- Separacja chronologii – umożliwienie pracy z kalendarzami innymi niż standardowe, np. japoński czy tajski.
Pomimo, że pakiet java.time, może być początkowo dosyć przerażający biorąc pod uwagę ilość klas jakie posiada, to nawet dla osoby z niedużym doświadczeniem nie będzie to ciężkie do ogarnięcia podstawowych przypadków. Podstawowe przypadki to jest jakieś 90% albo i więcej czasu naszej pracy (to już zależy jak trafisz). Do uzupełnienia pozostałych 10% będziesz musiał poczytać dokumentację, doczytać źródła w Internecie i popróbować samemu – nie jest to ciężkie, ale wymaga pracy.
Warto też dodać, że musisz rozumieć czym jest czas. Ale ten punkt powinieneś zrozumieć czytając kolejną sekcję.
A teraz krótki cytat z dokumentacji API:
This can seem like a lot of classes, but most applications can begin with just five date/time types.
Instant
– a timestampLocalDate
– a date without a time, or any reference to an offset or time-zoneLocalTime
– a time without a date, or any reference to an offset or time-zoneLocalDateTime
– combines date and time, but still without any offset or time-zoneZonedDateTime
– a „full” date-time with time-zone and resolved offset from UTC/Greenwich
Instant
is the closest equivalent class tojava.util.Date
.ZonedDateTime
is the closest equivalent class tojava.util.GregorianCalendar
.
Moment a data wydarzenia
Długo miałem problem żeby zrozumieć jaka jest różnica między momentem wydarzenia a datą wydarzenia. Mój mózg nie chciał tego zwizualizować. Jeżeli też masz problem ze zrozumieniem różnicy pomiędzy tym zagadnieniem, to proszę Cię o skupienie się przez najbliższą minutę.
Wyobraź sobie, że są Letnie Igrzyska Olimpijskie. Ludzie z całego świata walczą o jakieś tam tytuły w jednym konkretnym miejscu – akurat najbliższe będą w Tokyo. Inni ludzie – również z całego świata – będą te Igrzyska oglądać w różnych miejscach na świecie. Niech program Igrzysk trwa codziennie, pomiędzy 11 rano a 23 wieczorem czasu lokalnego (Tokyo).
Jestem Polakiem i chcę wiedzieć o której mam włączyć telewizor żeby móc to obejrzeć. Patrzę w reklamy w TV (tak nawiasem, nie oglądam TV) i mówią mi, że program Igrzysk trwa między 4 rano (dla niektórych to jeszcze noc) a 16. I właśnie tym jest moment. Konkretny czas wydarzenia w danym momencie na całej kuli ziemskiej (albo i we wszechświecie).
Kiedy stosować? Jeżeli Twoja aplikacja ma obsługiwać ludzi z całego świata (albo nawet z kilku stref czasowych) i chcesz żeby jakieś wydarzenie (cokolwiek co ma jakąś datę czy czas) było w danym momencie dostępne dla wszystkich o tej samej porze to użyj klas wspierających moment.
A co z datą wydarzenia? Wydaje mi się, że tutaj nie ma aż tak wielkich problemów ze zrozumieniem. Data wydarzenia jest po prostu konkretnym punktem na osi czasu, np. urodziny babci 23 czerwca, start Igrzysk o godzinie 11, piłka z kolegami w piątek o 20. I teraz pytanie – czy jeżeli urodziny babci są 23 czerwca a ja jestem w Tokyo i przypomniałem sobie o nich o godzinie 20 to zdążę jeszcze złożyć życzenia? No i tu znów wchodzi w grę moment.
To czy będziesz wspierał strefę czasową czy nie, zależy od specyfikacji Twojej aplikacji. Warto jednak pamiętać, że nawet jeżeli nie decydujesz się ich wspierać to możesz wpaść w problemy. Większość państw ma coś takiego jak czas letni i czas zimowy, gdzie godzina na zegarku przeskakuje o godzinę w przód lub w tył. Jeżeli chcesz dowiedzieć się więcej na temat problemów w jakie możesz wpaść stosując wyłącznie klasy nie wspierające momentu, obejrzyj to krótkie wideo.
Apropo stref czasowych i dlaczego sam nie powinieneś podejmować się ich implementacji. Polecam świetny 10-minutowy filmik przy którym można się nawet uśmiechnąć The Problem with Time & Timezones – Computerphile.
Dobre zalecenia
Niech ta sekcja będzie częścią wspólnych doświadczeń – jeżeli masz jakąś radę, coś, co widzisz, że jest problematyczne przy zarządzaniu czasem to daj o tym znać w komentarzu. Ja z chęcią zedytuję post i dodam Twoją radę.
Jeden. Modeluj domenę przy użyciu klas LocalDate, LocalTime i LocalDateTime.
Cytat z dokumentacji
Where possible, applications should use
LocalDate
,LocalTime
andLocalDateTime
to better model the domain. For example, a birthday should be stored in a codeLocalDate
. Bear in mind that any use of a time-zone, such as 'Europe/Paris’, adds considerable complexity to a calculation. Many applications can be written only usingLocalDate
,LocalTime
andInstant
, with the time-zone added at the user interface (UI) layer.
—
Dwa. Data i czas powinny być wymieniane/przechowywane w formacie UTC. Źródła [1][2]
(Uwaga! Patrz również punkt 4)
Choć na pierwszy rzut oka można sobie pomyśleć, że gryzie się to z punktem pierwszym bo zaraz zaraz… będę używać np. LocalDateTime, który jest czasem lokalnym i jednocześnie czasu UTC? Tak 🙂
1 2 | LocalDateTime nowInUTCTimeWithClock = LocalDateTime.now(Clock.systemUTC()); LocalDateTime nowInUTCTime = LocalDateTime.now(ZoneOffset.UTC); |
—
Trzy. Nie polegaj na strefie czasowej serwera!
Unikaj czegoś takiego:
1 2 | LocalDateTime.now(); LocalDateTime.now(Clock.systemDefaultZone()); |
Poleganie na domyślnej strefie czasowej może zaskoczyć – pamiętaj, że strefa czasowa serwera jest poza Twoją kontrolą jako programisty. Co jeżeli Twój program będzie hostowany na 2 lub 3 serwerach – każdy w innej strefie czasowej.
Możesz sobie powiedzieć, nie no – wiem, że jest tylko jedna instancja serwera i to w moim mieście. Ok… niech będzie. Ale co jeżeli tą instancją zarządza jakiś admin, któremu nagle przyjdzie do głowy, żeby zmienić strefę czasową na serwerze bo taki miał kapryś tego dnia. (Ignore Server Time Zone)
—
Cztery. UTC nie jest lekiem na wszystko!
Użytkownik Cepewka w komentarzach do tego posta słusznie zauważył że przechowywanie czasu jako UTC nie jest złotym środkiem. Istnieją sytuacje kiedy czas, przechowywany w taki sposób może nie być odpowiedni dla naszego przypadku biznesowego. Nie będę poruszać tych kwestii w tym poście ale chcę żeby każdy czytający był świadomy.
Zachęcam do przeczytania komentarzy użytkownika Cepewka (dzięki za poruszenie tej kwestii!). A jeżeli potrzebujesz więcej informacji to odsyłam do kilku linków:
- STORING UTC IS NOT A SILVER BULLET
- Is it always a good idea to store time in UTC or is this the case where storing in local time is better?
- Is there ever a good reason to store time not in UTC?
Podsumowanie
Zarządzanie czasem wydaje się łatwe ale nie jest proste. Lepiej spędź chwilę na rozmowach z klientem i podrąż temat jaki jest oczekiwany rezultat. Często okaże się, że plany długoterminowe biznesu są większe niż jest to przedstawiane na początku. Twórz takie API, które będzie łatwo przenaszalne pomiędzy różne serwery i na które strefa czasowa serwera nie wpływa. Mówi się, że czas leczy rany, w tym przypadku niekoniecznie 😅. Może się okazać, że z czasem przyjdą zmiany a my będziemy zbierali żniwo naszej wcześniejszej implementacji. Życzę, żeby tak nie było.
Źródła:
- How to get the current date/time in Java
- Working with Time Zones in Java 8 | ZonedDateTime, ZoneId tutorial with examples
- Still using java.util.Date? Don’t!
- MySQL Datetime vs Timestamp column types – which one I should use?
Za tydzień
Porozmawiamy o architekturze warstwowej. Jest to architektura która pozwala na szybkie dostarczanie? A może zmora przez którą nie myślimy o tym co dostarczamy?
Warto pamiętać, że trzymanie czasu jako UTC nie jest złotym środkiem. Jeżeli trzymamy instant jakiegoś wydarzenia w przyszłości (na przykład start konferencji za 2 lata), a w międzyczasie kraj zmieni strefę czasową lub reguły DST, to czas UTC się rozjedzie (najprawdopodobniej o +- jedną godzinę). Tak naprawdę trzeba trzymać czas UTC i wersję tzdb/cldr LUB trzymać trzymać czas w formie niezależnej od jakichkolwiek dat (na przykład string „9:00 rano czasu lokalnego”).
Hmm.. nie wiem czy dobrze ogarniam ale jeżeli przetrzymujemy czas w UTC i zarządzamy wyświetlaniem go to reguły konwertujące go na czas lokalny dla danego regionu się zaaplikują i wyświetlą poprawnie (o ile reguły w bibliotece java.time ulegną zmianie).
Dobrze myślę czy mylę koncepcję?
Bierzesz czas PL na 2022.07.25, dostajesz coś na kształt UTC + 2 (czy jaka tam strefa czasowa jest). Godzinę 9:00 UTC+2 traktujesz jako 7:00 w czasie UTC, czyli w bazie zapisujesz, że wydarzenie jest o 7 rano w czasie UTC. Teraz w 2021 Polska zmienia strefę czasową letnią na UTC+1 i robi się problem, bo przy wyświetleniu użytkownikowi wyciągniesz z bazy danych godzinę 7:00, dodasz godzinę (bo strefa czasowa w Polsce w lecie po zmianach to UTC+1) i w efekcie dostajesz 8:00. Powinieneś zapisać czas UTC wraz z wersją tzdb/cldr i albo aktualizować przy zmianach bazy albo przy wyświetlaniu odpowiednio… Czytaj więcej »
Teraz ogarniam, dzięki za wyjaśnienie ;). Za jakiś czas zaktualizuję post i dodam tą uwagę.
Przyznam się, że nawet nie rozkminiałem takiej sytuacji ale fakt faktem – jest realna i przy projektowaniu trzeba takie rzeczy uwzględniać
Problem jest tak naprawdę o wiele szerszy i łatwo się pomylić. Wyobraźmy sobie, że kupuję bilet lotniczy na grudzień tego roku na godzinę 8:00 w niedzielę. Ale za pół roku będę miał inne przesunięcie względem UTC (nie będę tu nawet wspominał o różnicach między strefą czasową a przesunięciem względem UTC itp, to jest bardzo duży temat). Jak teraz dodaję zdarzenie do kalendarza? Jeżeli kalendarz jest „głupi”, to przy przeglądaniu grudnia weźmie czas UTC, przeliczy względem obecnego czasu DST i pokaże mi, że wylot jest o 9:00, co wprawdzie nie jest „błędem” (bo za kilka miesięcy samo się naprawi), ale użytkownik… Czytaj więcej »
[…] Mądre zarządzanie czasem nie jest łatwe – w dużej mierze dotyczy się to życia prywatnego ale również ma swoje odniesienie w technologii. Każd […]