A few months ago I made a small Java library, which is worth explaining since the design of its classes and interfaces is pretty unusual. It’s very much object-oriented for a pretty imperative task: building a pipeline of document transformations. The goal was to do this in a declarative and immutable way, and in Java. Well, as much as it’s possible.
Let’s say you have a document, and you have a collection of transformations, each of which will do something with the document. Each transformation, for example, is a small piece of Java code. You want to build a list of transformations and then pass a document through this list.
First, I made an interface Shift
(instead of the frequently used and boring “transformation”):
interface Shift {
Document apply(Document doc);
}
Then I made an interface Train
(this is the name I made up for the collection of transformations) and its default implementation:
interface Train {
Train with(Shift shift);
Iterator<Shift> iterator();
}
class TrDefault implements Train {
private final Iterable<Shift> list;
@Override
Train with(Shift shift) {
final Collection<Shift> items = new LinkedList<>();
for (final Shift item : this.list) {
items.add(item);
}
items.add(shift);
return new TrDefault(items);
}
@Override
public Iterator<Shift> iterator() {
return this.list.iterator();
}
}
Ah, I forgot to tell you. I’m a big fan of immutable objects. That’s why the Train
doesn’t have a method add
, but instead has with
. The difference is that add
modifies the object, while with
makes a new one.
Now, I can build a train of shifts with TrDefault
, a simple default implementation of Train
, assuming ShiftA
and ShiftB
are already implemented:
Train train = new TrDefault()
.with(new ShiftA())
.with(new ShiftB());
Then I created an Xsline
class (it’s “XSL” + “pipeline”, since in my case I’m managing XML documents and transform them using XSL stylesheets). An instance of this class encapsulates an instance of Train
and then passes a document through all its transformations:
Document input = ...;
Document output = new Xsline(train).pass(input);
So far so good.
Now, I want all my transformations to log themselves. I created StLogged
, a decorator of Shift
, which encapsulates the original Shift
, decorates its method apply
, and prints a message to the console when the transformation is completed:
class StLogged implements Shift {
private final Shift origin;
@Override
Document apply(Document before) {
Document after = origin.apply(before);
System.out.println("Transformation completed!");
return after;
}
}
Now, I have to do this:
Train train = new TrDefault()
.with(new StLogged(new ShiftA()))
.with(new StLogged(new ShiftB()));
Looks like a duplication of new StLogged(
, especially with a collection of a few dozen shifts. To get rid of this duplication I created a decorator for Train
, which on the fly decorates shifts that it encapsulates, using StLogged
:
Train train = new TrLogged(new TrDefault())
.with(new ShiftA()))
.with(new ShiftB());
In my case, all shifts are doing XSL transformations, taking XSL stylesheets from files available in classpath. That’s why the code looks like this:
Train train = new TrLogged(new TrDefault())
.with(new StXSL("stylesheet-a.xsl")))
.with(new StXSL("stylesheet-b.xsl")));
There is an obvious duplication of new StXSL(...)
, but I can’t simply get rid of it, since the method with
expects an instance of Shift
, not a String
. To solve this, I made the Train
generic and created TrClasspath
decorator:
Train<String> train = new TrClasspath<>(new TrDefault<>())
.with("stylesheet-a.xsl"))
.with("stylesheet-b.xsl"));
TrClasspath.with()
accepts String
, turns it into StXSL
and passes to TrDefault.with()
.
Pay attention to the snippet above: the train
is now of type Train<String>
, not Train<Shift>
, as would be required by Xsline
. The question now is: how do we get back to Train<Shift>
?
Ah, I forgot to mention. I wanted to design this library with one important principle in mind, suggested in 2014: all objects may only implement methods from their interfaces. That’s why, I couldn’t just add a method getEncapsulatedTrain()
to TrClasspath
.
I introduced a new interface Train.Temporary<T>
with a single method back()
returning Train<T>
. The class TrClasspath
implements it and I can do this:
Train<Shift> train = new TrClasspath<>(new TrDefault<>())
.with("stylesheet-a.xsl"))
.with("stylesheet-b.xsl"))
.back();
Next I decided to get rid of the duplication of .with()
calls. Obviously, it would be easier to have the ability to provide a list of file names as an array of String
and build the train from it. I created a new class TrBulk
, which does exactly that:
Iterable<String> names = Arrays.asList(
"stylesheet-a.xsl",
"stylesheet-b.xsl"
);
Train<Shift> train = new TrBulk<>(
new TrClasspath<>(
new TrDefault<>()
)
).with(names).back();
With this design I can construct the train in almost any possible way.