Handling repeated code automatically
In this article I will describe how you can use Java::Geci generator
Repeated
to overcome the Java language shortage that generics cannot be primitive. The example is a suggested extension of the Apache Commons Lang library.
1. Introduction
When you copy-paste code you do something wrong. At least that is the perception. You have to create your code structure more generalized so that you can use different parameters instead of similar code many times.
This is not always the case. Sometimes you have to repeat some code because the language you use does not (yet) support the functionality that would be required for the problem.
This is too abstract. Let’s have a look at a specific example and how we can manage it using the Repeated
source generator, which runs inside the Java::Geci framework.
2. The problem
The class org.apache.commons.lang3.Functions
in the Apache Commons Lang library defines an inner interface FailableFunction
. This is a generic interface defined as
@FunctionalInterface
public interface FailableFunction<I, O, T extends Throwable> {
/**
* Apply the function.
* @param pInput the input for the function
* @return the result of the function
* @throws T if the function fails
*/
O apply(I pInput) throws T;
}
This is essentially the same as Function<I,O>
, which converts an I
to an O
but since the interface is failable, it can also throw an exception of type T
.
The new need is to have
public interface Failable<I>Function<O, T extends Throwable>
itnerfaces for each <I>
primitive values. The problem is that the generics cannot be primitive (yet) in Java, and thus we should separate interfaces for each primitive types, as
@FunctionalInterface
public interface FailableCharFunction<O, T extends Throwable> {
O apply(char pInput) throws T;
}
@FunctionalInterface
public interface FailableByteFunction<O, T extends Throwable> {
O apply(byte pInput) throws T;
}
@FunctionalInterface
public interface FailableShortFunction<O, T extends Throwable> {
O apply(short pInput) throws T;
}
@FunctionalInterface
public interface FailableIntFunction<O, T extends Throwable> {
O apply(int pInput) throws T;
}
... and so on ...
This is a lot of very similar methods that could easily be described by a template and then been generated by some code generation tool.
3. Template handling using Java::Geci
The Java::Geci framework comes with many off-the-shelf generators. One of them is the powerful Repeated
generator, which is exactly for this purpose. If there is a code that has to be repeated with possible parameters then you can define a template, the values and Repeated
will generate the code resolving the template parameters.
3.1. Adding dependency to the POM
The first thing we have to do is to add the Java::Geci dependencies to the pom.xml
file. Since Apache Commons Language is still Java 8 based we have to use the Java 8 backport of Java::Geci 1.2.0:
<dependency>
<groupId>com.javax1.geci</groupId>
<artifactId>javageci-core</artifactId>
<version>1.2.0</version>
<scope>test</scope>
</dependency>
Note that the scope of the dependency is test
. The generator Repeated
can conveniently be used without any Geci annotations that remain in the byte code and thus are compile-time dependencies. As a matter of fact, all of the generators can be used without annotations thus without any compile dependencies that would be an extra dependency for the production. In the case of Repeated
this is even easy to do.
3.2. Unit test to run the generator
The second thing we have to do is to create a unit test that will execute the generator. Java::Geci generators run during the unit test phase, so they can access the already compiled code using reflection as well as the actual source code. In case there is any code generated that is different from what was already there in the source file the test will fail and the build process should be executed again. Since generators are (should be) idempotent the test should not fail the second time.
As I experience, this workflow has an effect on the developer behavior, unfortunately. Run the test/ fails, run again! It is a bad cycle. Sometimes I happen to catch myself re-executing the unit tests when it was not a code generator that failed. However, this is how Java::Geci works.
There are articles about the Java::Geci workflow
so I will not repeat here the overall architecture and how its workflow goes.
The unit tests will be the following:
@Test
void generatePrimitiveFailables() throws Exception {
final Geci geci = new Geci();
Assertions.assertFalse(geci.source(Source.maven().mainSource())
.only("Functions")
.register(Repeated.builder()
.values("char,byte,short,int,long,float,double,boolean")
.selector("repeated")
.define((ctx, s) -> ctx.segment().param("Value", CaseTools.ucase(s)))
.build())
.generate(),
geci.failed());
}
The calls source()
, register()
and only()
configure the framework. This configuration tells the framework to use the source files that are in the main Java src
directory of the project and to use only the file names "Functions"
. The call to register()
registers the Repeated
generator instance right before we call generate()
that starts the code generation.
The generator instance itself is created using the built-in builder that lets us configure the generator. In this case, the call to values()
defines the comma-separated list of values with which we want to repeat the template (defined later in the code in a comment). The call to selector()
defines the identifier for this code repeated code. A single source file may contain several templates. Each template can be processed with a different list of values and the result will be inserted into different output segments into the source file. In this case there is only one such code generation template, still, it has to be identified with a name and this name has also to be used in the editor-fold
section which is the placeholder for the generated code.
The actual use of the name of the generator has two effects. One is that it identifies the editor fold segment and the template. The other one is that the framework will see the editor-fold segment with this identifier and it will recognize that this source file needs the attention of this generator. The other possibility would be to add the @Repeated
or @Geci("repeated")
annotation to the class.
If the identifier were something else and not repeated
then the source code would not be touched by the generator Repeated
or we would need another segment identified as repeated
, which would not actually be used other than trigger the generator.
The call to define()
defines a BiConsumer
that gets a context reference and an actual value. In this case, the BiConsumer
calculates the capitalized value and puts it into the actual segment parameter set associated with the name Value
. The actual value is associated with the name value
by default and the BiConsumer
passed to the method define()
can define and register other parameters. In this case, it will add new values as
value Value
char --> Char
byte --> Byte
short --> Short
int --> Int
long --> Long
float --> Float
double --> Double
boolean --> Boolean
3.3. Source Code
The third thing is to prepare the template and the output segment in the source file.
The output segment preparation is extremely simple. It is only an editor fold:
//<editor-fold id="repeated">
//</editor-fold>
The generated code will automatically be inserted between the two lines and the editors (Eclipse, IntelliJ or NetBeans) will allow you to close the fold. You do not want to edit this code: it is generated.
The template will look like the following:
/* TEMPLATE repeated
@FunctionalInterface
public interface Failable{{Value}}Function<O, T extends Throwable> {
O apply({{value}} pInput) throws T;
}
*/
The code generator finds the start of the template looking for lines that match the /* TEMPLATE name
format and collect the consecutive lines till the end of the comment.
The template uses the mustache template placeholder format, namely the name of the values enclosed between double braces. Double braces are rare in Java.
When we run the unit test it will generate the code that I already listed at the start of the article. (And after that it will fail of course: source code was modified, compile it again.)
4. Summary and Takeaway
The most important takeaway and WARNING: source code generation is a tool that aims to amend shortages of the programming language. Do not use code generations to amend a shortage that is not of the language but rather your experience, skill or knowledge about the language. The easy way to code generation is not an excuse to generate unnecessarily redundant code.
Another takeaway is that it is extremely easy to use this generator in Java. The functionality is comparable to the C preprocessor that Java does not have and for good. Use it when it is needed. Even though the setup of the dependencies and the unit test may be a small overhead later the maintainability usually pays this cost back.
Comments
Please leave your comments using Disqus, or just press one of the happy faces. If for any reason you do not want to leave a comment here, you can still create a Github ticket.