Sunday, 1 August 2010

Java: a fluent I/O API (1/4)

I've been experimenting with fluent API design. You can find the sources in part 4.

I've often been frustrated with the verbosity of Java I/O. Handling close with decorators got better with the introduction of the Closeable interface, but there's still a bit of boilerplate. This post describes a new fluent API to wrapper around the existing I/O API.

Goals

The goals of this API are:

  • Reduce the amount of code required for I/O operations.
  • Be extensible.
  • Interoperate with the existing java.io API and any 3rd party frameworks that build on it (like Guava and Apache Commons I/O).
  • Reduce the opportunities for resource leaks and poor error handling.
  • Encourage good character data transcoding.
  • Add some support for runtime exception handling.
  • Be small, clean and easily understood.

Terminology

  • Doctor: to revise, alter, or adapt in order to serve a specific purpose or to improve the material
  • Die: any of various devices for cutting or forming material in a press or a stamping or forging machine

Traditional Java I/O

The following method replaces all the newlines in a UTF-8 text input file with unix-style \n newlines.

  private static void transform(File src, File destthrows IOException {
    Charset utf8 = Charset.forName("UTF-8");
    InputStream in = new FileInputStream(src);
    Closeable inputResource = in;
    try {
      Reader reader = new InputStreamReader(in, utf8);
      inputResource = reader;
      BufferedReader buffer = new BufferedReader(reader);
      inputResource = buffer;
      OutputStream out = new FileOutputStream(dest);
      Closeable outputResource = out;
      try {
        Writer writer = new OutputStreamWriter(out, utf8);
        outputResource = writer;
        replaceNewLines(buffer, writer);
      finally {
        outputResource.close();
      }
    finally {
      inputResource.close();
    }
  }

  private static void replaceNewLines(BufferedReader src, Writer dest)
      throws IOException {
    for (String line = src.readLine(); line != null; line = src.readLine()) {
      dest.write(line);
      dest.write("\n");
    }
  }

This code does its best to be bullet-proof. Even if a stream constructor were to throw a RuntimeException, the use of Closeables ensures that the contract for the preceding streams is enforced and they are closed properly. The downside: a load of extra lines and a nested try-finally block.

Better closing

It is possible to leverage the Closeable interface to eliminate some of this code. Here is the method rewritten to use Closers from the new API:

  private static void closingTransform(File src, File destthrows IOException {
    Charset utf8 = Charset.forName("UTF-8");
    CoupledCloser closer = Closers.coupledCloser();
    try {
      InputStream in = closer.in().using(new FileInputStream(src));
      Reader reader = closer.in().using(new InputStreamReader(in, utf8));
      BufferedReader buffer = closer.in().using(new BufferedReader(reader));
      OutputStream out = closer.out().using(new FileOutputStream(dest));
      Writer writer = closer.out().using(new OutputStreamWriter(out, utf8));
      replaceNewLines(buffer, writer);
    finally {
      closer.close();
    }
  }

The CoupledCloser type just holds two Closer instances. A Closer just holds a reference to the last Closeable passed to its using method.

This code manages to eliminate a try-finally block. The calls are compressed onto fewer lines, but it is arguable whether this code is clearer.

Fluent I/O

Here is the transform method rewritten to use the new API:

  private static void fluentTransform(File src, File destthrows IOException {
    CoupledCloser closer = Closers.coupledCloser();
    try {
      Iterable<String> lines = IO.access(closer.in(), src)
                                 .buffered()
                                 .utf8()
                                 .doctor(Doctors.LINES);
      
      Writer writer = IO.open(closer.out(), dest)
                        .buffered()
                        .utf8()
                        .die();
      
      replaceNewLines(lines, writer);
    finally {
      closer.close();
    }
  }

  private static void replaceNewLines(Iterable<String> src, Writer dest)
      throws IOException {
    for (String line : src) {
      dest.write(line);
      dest.write("\n");
    }
  }

To make the type system visible, here is the code broken down into individual calls:

  private static void transformOp(File src, File destthrows IOException {
    CoupledCloser closer = Closers.coupledCloser();
    try {
      Closer in = closer.in();
      InputDie<FileInputStream> accessed = IO.access(in, src);
      InputDie<BufferedInputStream> bufferIn = accessed.buffered();
      ReaderDie<InputStreamReader> readerDie = bufferIn.utf8();
      Iterable<String> lines = readerDie.doctor(Doctors.LINES);
      Closer out = closer.out();
      OutputDie<FileOutputStream> opened = IO.open(out, dest);
      OutputDie<BufferedOutputStream> bufferOut = opened.buffered();
      WriterDie<OutputStreamWriter> writerDie = bufferOut.utf8();
      Writer writer = writerDie.die();
      replaceNewLines(lines, writer);
    finally {
      closer.close();
    }
  }

This code is obviously way worse than just using the core API, so it should be clear why sticking to the fluent style is prefereable.

The doctor method illustrates the main extensibility mechanism in the API. By writing Doctor interface implementations, you can doctor stream types in any manner you see fit. Since no I/O API can cover every conceivable stream decorator, this is a necessary compromise to the fluent design pattern.

Related posts

Comments & criticism are welcome.

No comments:

Post a Comment

All comments are moderated