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 . |