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 знает, как писать, а никто другой не знает. Функция записи в файл инкапсулирована, и это позволяет декорировать объекты любым возможным способом, создавая многократно используемые и заменяемые составные объекты.
Вы видите красоту ООП сейчас?
| Ссылка: | Объектно-ориентированный декларативный ввод / вывод в Cactoos от нашего партнера по JCG Егора Бугаенко в блоге About Programming . |