Статьи

Печать даты / времени тоже может быть элегантной

В частности, я обязан своей довольно высокой репутацией StackOverflow этому вопросу , который я задал несколько лет назад: как вы печатаете дату ISO 8601 на Java? С тех пор удалось собрать много откликов и более 20 ответов, включая мой собственный . Серьезно, почему у Java, такой богатой экосистемы, не было встроенного простого решения для этой примитивной задачи? Я полагаю, что это потому, что разработчики Java SDK были 1) достаточно умны, чтобы не создавать метод print() прямо в классе Date , и 2) не достаточно умны, чтобы дать нам расширяемый набор классов и интерфейсов для анализа и печати. даты в элегантном стиле.

Фарго, 2 сезон (2014) от Ноа Хоули

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