Статьи

Объектно-ориентированный декларативный ввод / вывод в Cactoos

Cactoos — это библиотека объектно-ориентированных Java-примитивов, над которой мы начали работать всего несколько недель назад. Намерение состояло в том, чтобы предложить чистую и более декларативную альтернативу JDK, Guava, Apache Commons и другим. Вместо вызова статических процедур мы хотим использовать объекты так, как они должны использоваться. Давайте посмотрим, как ввод / вывод работает чисто объектно-ориентированным способом.

Допустим, вы хотите прочитать файл. Вот как это можно сделать с помощью статического метода readAllBytes() из служебного класса Files в JDK7:

1
2
3
byte[] content = Files.readAllBytes(
  new File("/tmp/photo.jpg").toPath()
);

Этот код очень важен — он читает содержимое файла прямо здесь и сейчас, помещая его в массив.

Вот как вы делаете это с Cactoos :

1
2
3
4
5
Bytes source = new InputAsBytes(
  new FileAsInput(
    new File("/tmp/photo.jpg")
  )
);

Обратите внимание — пока нет вызовов методов. Всего три конструктора или три класса, которые составляют более крупный объект. Источник объекта имеет тип Bytes и представляет содержимое файла. Чтобы извлечь это содержимое, мы вызываем его метод asBytes() :

1
bytes[] content = source.asBytes();

Это момент, когда файловая система затрагивается. Этот подход, как видите, абсолютно декларативен и благодаря этому обладает всеми преимуществами объектной ориентации.

Вот еще один пример. Скажем, вы хотите написать текст в файл. Вот как вы делаете это в Cactoos. Сначала вам нужен Input :

1
2
3
4
5
6
7
Input input = new BytesAsInput(
  new TextAsBytes(
    new StringAsText(
      "Hello, world!"
    )
  )
);

Тогда вам нужен Output :

1
2
3
Output output = new FileAsOutput(
  new File("/tmp/hello.txt")
);

Теперь мы хотим скопировать ввод в вывод. В чистом ООП нет операции «копировать». Более того, не должно быть никаких операций вообще. Просто объекты. У нас есть класс с именем TeeInput , который представляет собой Input который копирует все, что вы читаете из него, в Output , аналогично тому, что TeeInputStream из Apache Commons , но инкапсулируется. Поэтому мы не копируем, мы создаем Input который будет копировать, если вы прикоснетесь к нему:

1
Input tee = new TeeInput(input, output);

Теперь мы должны «потрогать» это. И мы должны коснуться каждого его байта, чтобы убедиться, что они все скопированы. Если мы только read() первый байт, только один байт будет копией в файл. Лучший способ прикоснуться к ним всем — это рассчитать размер tee объекта, идущего побайтово. Для этого у нас есть объект, который называется LengthOfInput . Он инкапсулирует Input и ведет себя как его длина в байтах:

1
Scalar<Long> length = new LengthOfInput(tee);

Затем мы берем это значение и выполняем операцию записи файла:

1
long len = length.asValue();

Таким образом, вся операция записи строки в файл будет выглядеть так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
new LengthOfInput(
  new TeeInput(
    new BytesAsInput(
      new TextAsBytes(
        new StringAsText(
          "Hello, world!"
        )
      )
    ),
    new FileAsOutput(
      new File("/tmp/hello.txt")
    )
  )
).asValue(); // happens here

Это его процедурная альтернатива из JDK7 :

1
2
3
4
Files.write(
  new File("/tmp/hello.txt").toPath(),
  "Hello, world!".getBytes()
);

«Почему объектно-ориентированный лучше, хотя он длиннее?» Я слышал, вы спрашиваете. Потому что он отлично разделяет понятия, а процедурный объединяет их.

Допустим, вы разрабатываете класс, который должен зашифровать некоторый текст и сохранить его в файл. Вот как бы вы разработали это процедурным способом (конечно, не настоящим шифрованием):

01
02
03
04
05
06
07
08
09
10
11
12
class Encoder {
  private final File target;
  Encoder(final File file) {
    this.target = file;
  }
  void encode(String text) {
    Files.write(
      this.target,
      text.replaceAll("[a-z]", "*")
    );
  }
}

Работает нормально, но что произойдет, когда вы решите расширить его, чтобы также записать в OutputStream ? Как вы будете изменять этот класс? Насколько некрасиво это будет выглядеть после этого? Это потому, что дизайн не является объектно-ориентированным.

Вот как вы могли бы сделать тот же дизайн объектно-ориентированным способом с Cactoos :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Encoder {
  private final Output target;
  Encoder(final File file) {
    this(new FileAsOutput(file));
  }
  Encoder(final Output output) {
    this.target = output;
  }
  void encode(String text) {
    new LengthOfInput(
      new TeeInput(
        new BytesAsInput(
          new TextAsBytes(
            new StringAsText(
              text.replaceAll("[a-z]", "*")
            )
          )
        ),
        this.target
      )
    ).asValue();
  }
}

Что мы делаем с этим проектом, если мы хотим, чтобы OutputStream был принят? Мы просто добавляем один вторичный конструктор:

1
2
3
4
5
class Encoder {
  Encoder(final OutputStream stream) {
    this(new OutputStreamAsOutput(stream));
  }
}

Готово. Вот как это просто и элегантно.

Это потому, что концепции идеально разделены и функциональность заключена в капсулу. В процедурном примере поведение объекта находится вне его, в методе encode() . Сам файл не знает, как писать, некоторая внешняя процедура Files.write() знает об этом.

Напротив, в объектно-ориентированном дизайне FileAsOutput знает, как писать, а никто другой не знает. Функция записи в файл инкапсулирована, и это позволяет декорировать объекты любым возможным способом, создавая многократно используемые и заменяемые составные объекты.

Вы видите красоту ООП сейчас?