Wraz z zespołem, w którym pracuję, niedawno utworzyliśmy nową funkcjonalność. Nie jest istotne co ona dokładnie robi. Ważne jest, że jednym z jej parametrów wejściowych jest data. Program w trakcie działania weryfikuje czy podany parametr pasuje do formatu YYYYMMDDhhmm[ss]
oraz czy znajduje się w przedziale czasowym +/- 15 minut od bieżącego czasu. Dodam, że taki format został narzucony z góry przez zewnętrzny standard. Podczas testów nowego feature’a klient zadał nam pytanie dla jakiej strefy czasowej ma zostać podana data. To wprowadziło nas w konsternację. Nie dlatego, że nie dało się podać strefy czasowej na wejściu, lecz jak faktycznie daty w programie są ze sobą porównywane. Koniec końców wyszło, że aplikacja działała poprawnie, jednak implementacja nie do końca przypadła mi do gustu, zaraz wyjaśnię dlaczego.
Przyjrzenie się problemowi
Implementację powinno rozpocząć się zaraz po zapoznaniu i zrozumieniu wymagań. Z nich jasno wynikało, że aplikacja ma otrzymywać jako parametr datę czasu lokalnego polskiego. Z tego powodu kompletnie nie powinna interesować nas weryfikacja stref czasowych. Nasuwa się, więc pytanie dlaczego klient miał wątpliwości. Możliwe, że testując naszą funkcjonalność dostawał błąd pomimo podania prawidłowych danych. Należało to zweryfikować.
Załóżmy, że na wejściu otrzymujemy String’a o wartości 20201008164325
, który podlega parsowaniu przez dostarczoną, zewnętrzną bibliotekę. Zaglądając w bebechy metody parsującej otrzymujemy informację, że zwraca ona nowy obiekt klasy Timestamp. Patrząc uważniej można zaobserwować, że domyślnie dokleja ona dodatkowo do naszej instancji domyślną strefę czasową UTC! Od razu nasuwa się pytanie: “Dlaczego?!”. Przecież nazwa tej klasy nic takiego nie sugeruje!
Dobrze, ale pomińmy kwestię nietrafnego nazewnictwa klas biblioteki zewnętrznej. Może to właśnie dodanie zbędnej strefy czasowej było przyczyną powstania pytania klienta. Jednym z możliwych scenariuszy jest to, że zewnętrzna biblioteka ustawia daną strefę czasową przy parsowaniu, a nasza logika pobiera aktualną datę dla innej strefy czasowej. W ten sposób weryfikując zakres +/- 15 minut nigdy nie spełnimy tego warunku, bo strefy mogą się różnić minimalnie o 30 minut (poza ekstremalnymi przypadkami Nepalu i Nowej Zelandii 😉).
Dalsza analiza przypadku
Na szczęście nasza funkcjonalność używa LocalDateTime
. Zaglądając do dokumentacji Javy można znaleźć taki fragment definicji tej klasy:
A date-time without a time-zone in the ISO-8601 calendar system, such as 2007-12-03T10:15:30.
Dzięki czemu uzyskujemy datę aktualnego czasu lokalnego bez strefy czasowej. Dodatkowo przy pomocy poniższego zabiegu jesteśmy w stanie zmienić datę zewnętrznej biblioteki również na klasę LocalDateTime
(“Z” oznacza Uniwersalny Czas Koordynowany, czyli UTC).
1
2
Instant instant = Instant.ofEpochMilli(data.getMillis());
LocalDateTime data = LocalDateTime.ofInstant(instant, ZoneId.of("Z"));
Wyjaśnienia wymaga tutaj jedna kwestia. Biblioteka zewnętrza dostarcza nam metodę getMillis()
, która opatrzona jest komentarzem /** The millis from 1970-01-01T00:00:00Z */
. Z tego powodu podając strefę “Z” dla Instant.ofEpochMilli()
otrzymaliśmy poprawną datę w czasie lokalnym polskim. Wychodzi na to, że program poprawnie porównuje daty. Klient zatem został poinformowany, że funkcjonalność powinna działać bez zarzutów.
Podsumowanie
Faktycznie wszystko nadal działa prawidłowo. Jednak czy to jest poprawna implementacja? Dlaczego dokonywane są takie konwersje? Skoro wymaganie mówi o tym, że podana data musi znajdować się w czasie lokalnym polskim to powinno się ją sparsować przy pomocy LocalDateTime
i porównać z aktualnym czasem. Jeśli natomiast jesteśmy uzależnieni od zewnętrznej biblioteki to może warto przemyśleć wzorzec Anti-corruption layer, aby odciąć się od takich nieścisłości.
Jakie jest Twoje zdanie na ten temat? Może potrafisz zaproponować jakieś inne rozwiązanie dla tego problemu. Daj znać w komentarzu lub napisz do mnie osobiście.