В частности, я обязан своей довольно высокой репутацией StackOverflow этому вопросу , который я задал несколько лет назад: как вы печатаете дату ISO 8601 на Java? С тех пор удалось собрать много откликов и более 20 ответов, включая мой собственный . Серьезно, почему у Java, такой богатой экосистемы, не было встроенного простого решения для этой примитивной задачи? Я полагаю, что это потому, что разработчики Java SDK были 1) достаточно умны, чтобы не создавать метод print()
прямо в классе Date
, и 2) не достаточно умны, чтобы дать нам расширяемый набор классов и интерфейсов для анализа и печати. даты в элегантном стиле.
Есть три основных способа разделить ответственность за анализ и печать в JDK (насколько мне известно):
DTO + Сервисный класс
Первый — когда что-то отвечает за печать и синтаксический анализ, а объект — это просто держатель данных. Существует класс SimpleDateFormat
, который должен быть настроен первым, с правильным часовым поясом и шаблоном форматирования. Затем он должен быть использован для печати:
1
2
3
|
DateFormat df = new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm'Z'" ); df.setTimeZone(TimeZone.getTimeZone( "UTC" )); String iso = df.format( new Date()); |
Чтобы разобрать его, есть метод parse()
:
1
|
Date date = df.parse( "2007-12-03T10:15Z" ); |
Это классическая комбинация DTO и служебного класса . DTO — это объект Date
а служебный класс — SimpleDateFormat
. Объект date предоставляет все необходимые атрибуты данных через несколько получателей, а служебный класс печатает дату. Дата-объект не имеет никакого влияния на этот процесс. На самом деле это не объект, а просто контейнер данных. Это не объектно-ориентированное программирование вообще.
Предмет
Java 8 представила класс Instant
с методом toString()
, который возвращает время в формате ISO-8601 :
1
|
String iso = Instant.now().toString(); |
Для его анализа есть статический метод parse()
в том же классе Instant
:
1
|
Instant time = Instant.parse( "2007-12-03T10:15:30Z" ); |
Этот подход выглядит более объектно-ориентированным, но проблема здесь в том, что невозможно каким-либо образом изменить шаблон печати (например, удалить миллисекунды или полностью изменить формат). Более того, метод parse()
является статическим , что означает, что не может быть полиморфизма — мы не можем изменить логику синтаксического анализа. Мы также не можем изменить логику печати, поскольку Instant
— это последний класс, а не интерфейс.
Этот дизайн звучит нормально, если все, что нам нужно, это строки даты / времени ISO 8601. В тот момент, когда мы решили расширить его, мы попадаем в беду.
Гадкий Микс
В Java 8 также есть DateTimeFormatter
, который представляет третий способ работы с объектами даты / времени. Чтобы напечатать дату в String
мы создаем экземпляр «форматера» и передаем его объекту времени:
1
2
3
4
5
|
LocalDateTime date = LocalDateTime.now(ZoneId.of( "UTC" )); DateTimeFormatter formatter = DateTimeFormatter.ofPattern( "yyyy-MM-dd'T'HH:mm:ss'Z'" ); String iso = time.format(formatter); |
Чтобы разобрать, мы должны отправить formatter
статическому методу parse()
вместе с текстом для разбора:
1
|
LocalDateTime time = LocalDateTime.parse( "2007-12-03T10:15:30Z" , formatter); |
Как они общаются, LocalDateTime
и DateTimeFormatter
? Объект времени — это TemporalAccessor
, с методом get()
позволяющим любому извлекать то, что находится внутри. Другими словами, снова DTO . Форматтер все еще является служебным классом (даже не интерфейсом), который ожидает прибытия DTO, извлекает то, что находится внутри, и печатает.
Как они разбираются? Метод parse()
читает шаблон, а также создает и возвращает еще один DTO TemporalAccessor
.
Как насчет инкапсуляции? «Не в этот раз», — говорят дизайнеры JDK.
Правильный путь
Вот как я бы разработал это вместо этого. Во-первых, я бы сделал общий неизменный Template
с этим интерфейсом:
1
2
3
4
|
interface Template { Template with(String key, Object value); Object read(String key); } |
Это будет использовано так:
1
2
3
4
5
6
7
8
|
String iso = new DefaultTemplate( "yyyy-MM-dd'T'HH:mm'Z'" ) .with( "yyyy" , 2007 ) .with( "MM" , 12 ) .with( "dd" , 03 ) .with( "HH" , 10 ) .with( "mm" , 15 ) .with( "ss" , 30 ) .toString(); // returns "2007-12-03T10:15Z" |
Этот шаблон внутренне решает, как печатать поступающие данные, в зависимости от инкапсулированного шаблона. Вот как Date
могла бы напечатать себя:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
class Date { private final int year; private final int month; private final int day; private final int hours; private final int minutes; private final int seconds; Template print(Template template) { return template .with( "yyyy" , this .year) .with( "MM" , this .month) .with( "dd" , this .day) .with( "HH" , this .hours) .with( "mm" , this .minutes) .with( "ss" , this .seconds); } |
Вот как будет работать синтаксический анализ (вообще плохая идея помещать код в конструктор, но для этого эксперимента все в порядке):
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
class Date { private final int year; private final int month; private final int day; private final int hours; private final int minutes; private final int seconds; Date(Template template) { this .year = template.read( "yyyy" ); this .month = template.with( "MM" ); this .day = template.with( "dd" ); this .hours = template.with( "HH" ); this .minutes = template.with( "mm" ); this .seconds = template.with( "ss" ); } |
Допустим, мы хотим напечатать время как «13-е января 2019 года» (на русском языке). Как бы мы это сделали? Мы не создаем новый Template
, мы украшаем существующий несколько раз. Во-первых, мы делаем пример того, что у нас есть:
1
|
new DefaultTemplate( "dd-е MMMM yyyy-го года" ) |
Этот напечатает что-то вроде этого:
1
|
12 -е MMMM 2019 -го года |
Date
не отправляет в него значение MMMM
, поэтому не корректно заменяет текст. Мы должны украсить его:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
class RussianTemplate { private final Template origin; RussianTemplate(Template t) { this .origin = t; } @Override Template with(String key, Object value) { Template t = this .origin.with( "MM" , value); if (key.equals( "MM" )) { String name = "" ; switch (value) { case 0 : name = "января" ; break ; case 1 : name = "февраля" ; break ; // etc... } t = t.with( "MMMM" , name); } return t; } } |
Теперь, чтобы получить русскую дату из объекта Date
мы делаем это:
1
2
3
4
5
|
String txt = time.print( new RussianTemplate( new DefaultTemplate( "dd-е MMMM yyyy-го года" ) ) ); |
Допустим, мы хотим напечатать дату в другом часовом поясе. Мы создаем другой декоратор, который перехватывает вызов с помощью "HH"
и вычитает (или добавляет) разницу во времени:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
class TimezoneTemplate { private final Template origin; private final int zone; RussianTemplate(Template t, int z) { this .origin = t; this .zone = z } @Override Template with(String key, Object value) { Template t = this .origin.with( "MM" , value); if (key.equals( "HH" )) { t = t.with( "MM" , Integer.cast(value) + this .z); } return t; } } |
Этот код напечатает московское (UTC + 3) время на русском языке:
1
2
3
4
5
6
7
8
|
String txt = time.print( new TimezoneTemplate( new RussianTemplate( new DefaultTemplate( "dd-е MMMM yyyy-го года" ) ), + 3 ) ); |
Мы можем украшать столько, сколько нам нужно, делая Template
настолько мощным, насколько это необходимо. Элегантность этого подхода заключается в том, что класс Date
полностью отделен от Template
, что делает их как заменяемыми, так и полиморфными.
Может быть, кому-то будет интересно создать библиотеку для печати и анализа даты и времени с открытым исходным кодом с учетом этих принципов?
Опубликовано на Java Code Geeks с разрешения Егора Бугаенко, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Печать даты / времени тоже может быть элегантной Мнения, высказанные участниками Java Code Geeks, являются их собственными. |