In part one of this series, I’ve
introduced you to the Transformation
interface, which applies or unapplies a function to a Source
or Sink
.
In this posting, I’ll show you how to write a general-purpose composed Transformation
.
The take-away-lesson to learn from part one was that a Transformation
works like a small Lego brick:
If you ever played with Lego bricks, then you know the simple bricks with just a few knobs are the most versatile
because you can compose them in an almost unlimited fashion.
The same principle applies to the Transformation
interface:
With only two methods, a Transformation
is simple to implement and use, which makes it almost as versatile than a Lego
brick with only two knobs.
To see how you can generally compose transformations into a new one, let’s revisit how I used two transformations in order to compress and encrypt some data. First, let’s assume the following:
Transformation compression = new Compression();
Transformation encryption = new PbeEncryption(password);
To apply a compression-then-encryption schema, I would write the following code (assuming Java 7):
Sink sink = ... // some Sink
Sink composition = compression.apply(encryption.apply(sink));
try (OutputStream out = composition.output()) {
... // happily write some data
}
To do the inverse when reading data, I would write the following code:
Source source = ... // some Source
Source composition = compression.unapply(encryption.unapply(source));
try (InputStream in = composition.input()) {
... // happily read some data
}
Do you spot the pattern? In both cases, I manually composed two transformations (one for compression, one for encryption) into a larger one before doing I/O. This pattern generally applies to whatever two transformations I would want to compose, so let’s write some code to encapsulate it:
public class Transformations {
public static Transformation compose(
final Transformation primary,
final Transformation secondary) {
return new Transformation() {
@Override public Sink apply(Sink sink) {
return primary.apply(secondary.apply(sink));
}
@Override public Source unapply(Source source) {
return primary.unapply(secondary.unapply(source));
}
};
}
private Transformations() { }
}
Now I can compose the two transformations as follows:
Transformation composition = Transformations.compose(compression, encryption);
Given this composition, I can shorten the previous example for writing compressed-then-encrypted data to:
Sink sink = ... // some Sink
try (OutputStream out = composition.apply(sink).output()) {
... // happily write some data
}
… and similar for reading the data back:
Source source = ... // some Source
try (InputStream in = composition.unapply(source).input()) {
... // happily read some data
}
The beauty of this is that the composed transformation nicely encapsulates all configuration required for the compression and encryption algorithms into a single, immutable object which you can apply or unapply as often as you need to.
Enjoy!