В частности, я обязан своей довольно высокой репутацией 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, являются их собственными. |