Declarative and Immutable Pipeline of Transformations
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”):
Then I made an interface Train
(this is the name I made up for the collection of transformations) and its default implementation:
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:
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:
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:
Now, I have to do this:
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
:
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:
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:
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:
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:
With this design I can construct the train in almost any possible way.
See, for example, how we use it here and here.
Published on Java Code Geeks with permission by Yegor Bugayenko, partner at our JCG program. See the original article here: Declarative and Immutable Pipeline of Transformations Opinions expressed by Java Code Geeks contributors are their own. |