Object-Oriented Declarative Input/Output in Cactoos
Cactoos is a library of object-oriented Java primitives we started to work on just a few weeks ago. The intent was to propose a clean and more declarative alternative to JDK, Guava, Apache Commons, and others. Instead of calling static procedures we want to use objects, the way they are supposed to be used. Let’s see how input/output works in a pure object-oriented fashion.
Let’s say you want to read a file. This is how you would do it with the static method readAllBytes()
from the utility class Files
in JDK7:
byte[] content = Files.readAllBytes( new File("/tmp/photo.jpg").toPath() );
This code is very imperative—it reads the file content right here and now, placing it into the array.
This is how you do it with Cactoos:
Bytes source = new InputAsBytes( new FileAsInput( new File("/tmp/photo.jpg") ) );
Pay attention—there are no method calls yet. Just three constructors or three classes that compose a bigger object. The object source
is of type Bytes
and represents the content of the file. To get that content out of it we call its method asBytes()
:
bytes[] content = source.asBytes();
This is the moment when the file system is touched. This approach, as you can see, is absolutely declarative and thanks to that possesses all the benefits of object orientation.
Here is another example. Say you want to write some text into a file. Here is how you do it in Cactoos. First you need the Input
:
Input input = new BytesAsInput( new TextAsBytes( new StringAsText( "Hello, world!" ) ) );
Then you need the Output
:
Output output = new FileAsOutput( new File("/tmp/hello.txt") );
Now, we want to copy the input to the output. There is no “copy” operation in pure OOP. Moreover, there must be no operations at all. Just objects. We have a class named TeeInput
, which is an Input
that copies everything you read from it to the Output
, similar to what TeeInputStream
from Apache Commons does, but encapsulated. So we don’t copy, we create an Input
that will copy if you touch it:
Input tee = new TeeInput(input, output);
Now, we have to “touch” it. And we have to touch every single byte of it, in order to make sure they all are copied. If we just read()
the first byte, only one byte will be copies to the file. The best way to touch them all is to calculate the size of the tee
object, going byte by byte. We have an object for it, called LengthOfInput
. It encapsulates an Input
and behaves like its length in bytes:
Scalar<Long> length = new LengthOfInput(tee);
Then we take the value out of it and the file writing operation takes place:
long len = length.asValue();
Thus, the entire operation of writing the string to the file will look like this:
new LengthOfInput( new TeeInput( new BytesAsInput( new TextAsBytes( new StringAsText( "Hello, world!" ) ) ), new FileAsOutput( new File("/tmp/hello.txt") ) ) ).asValue(); // happens here
This is its procedural alternative from JDK7:
Files.write( new File("/tmp/hello.txt").toPath(), "Hello, world!".getBytes() );
“Why is object-oriented better, even though it’s longer?” I hear you ask. Because it perfectly decouples concepts, while the procedural one keeps them together.
Let’s say, you are designing a class that is supposed to encrypt some text and save it to a file. Here is how you would design it the procedural way (not a real encryption, of course):
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]", "*") ); } }
Works fine, but what will happen when you decide to extend it to also write to an OutputStream
? How will you modify this class? How ugly will it look after that? That’s because the design is not object-oriented.
This is how you would do the same design, in an object-oriented way, with Cactoos:
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(); } }
What do we do with this design if we want OutputStream
to be accepted? We just add one secondary constructor:
class Encoder { Encoder(final OutputStream stream) { this(new OutputStreamAsOutput(stream)); } }
Done. That’s how easy and elegant it is.
That’s because concepts are perfectly separated and functionality is encapsulated. In the procedural example the behavior of the object is located outside of it, in the method encode()
. The file itself doesn’t know how to write, some outside procedure Files.write()
knows that instead.
To the contrary, in the object-oriented design the FileAsOutput
knows how to write, and nobody else does. The file writing functionality is encapsulated and this makes it possible to decorate the objects in any possible way, creating reusable and replaceable composite objects.
Do you see the beauty of OOP now?
Reference: | Object-Oriented Declarative Input/Output in Cactoos from our JCG partner Yegor Bugayenko at the About Programming blog. |