Handling Repeated Code Automatically
The easy way to code generation is not an excuse to generate unnecessarily redundant code.
Join the DZone community and get the full member experience.
Join For FreeIn this article, I will describe how you can use the 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.
When you copy-paste code, you do something wrong. At least that is the perception. You have to create your code structure in a more generalized way so that you can use different parameters instead of similar code many times.
You may also like: Your Code Is Redundant, Live With It!
This is not always the case. Sometimes, you have to repeat 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 see how we can manage it using the Repeated
source generator, which runs inside the Java::Geci framework.
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>
Interfaces 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 ...
There are a lot of very similar methods that could easily be described by a template and then be generated by a code generation tool.
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.
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.
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 (and 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 other articles about the Java::Geci workflow:
- Your Code Is Redundant, Live With It!
- Tools to Keep JavaDoc Up-to-Date
- Converting Objects to Map and Back
- Reflection Selector Expression
- Generating Getters and Setters Using Java::Geci
- Creating a Java::Geci Generator
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
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 until 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, and the source code was modified, so compile it again.)
The most important takeaway and WARNING is this: 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 pre-processor 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.
Further Reading
Published at DZone with permission of Peter Verhas, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Trending
-
Observability Architecture: Financial Payments Introduction
-
Top 10 Pillars of Zero Trust Networks
-
VPN Architecture for Internal Networks
-
An Overview of Kubernetes Security Projects at KubeCon Europe 2023
Comments