Object-Oriented Declarative Input/Output in Cactoos

Cactoos is a library of object-oriented Java primitives westarted to work on just a few weeks ago. The intent was to propose a clean and more declarative alternative to JDKGuavaApache 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.

Disclaimer: The version I'm using at the time of writing is 0.9. Later versions may have different names of classes and a totally different design.

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 of three classes that compose a bigger object. The object source is of type Bytesand 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 copied 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.value();

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")
    )
  )
).value(); // 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
      )
    ).value();
  }
}

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?



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值