Groovy - Intention revealing code with map constructors
"Effective Java" recommends using static factory methods in Java instead of constructors. One reason for this is that static factory methods do a better job at intention revealing than regular constructors. This is because if you have constructors with many arguments, that too multiple arguments with the same data type, it is hard to remember their positions. Modern IDEs such as IntelliJ IDEA help address this issue by providing a type and parameter name hint when you invoke constructors.
In this post, we explore what Groovy does to support revealing the constructor's intentions. Groovy supports map constructors. This means we can create a constructor that accepts a map as an argument. When we invoke the constructor, we pass a map argument with the parameter name as the key and the corresponding argument as the value. This way, one does not need to remember the order of arguments.
class Developer {
String firstName
String lastName
String department
List<String> skills = new ArrayList<>()
Developer(String firstName, String lastName, String department, List<String> skills) {
this.firstName = firstName
this.lastName = lastName
this.department = department
this.skills = skills
}
}
def developer = new Developer('Naresha', 'Bhat', 'Insurance', ['Groovy', 'Java', 'JavaScript'])
Let us start with the classic constructor. Here, to invoke the constructor, we have to pass the arguments in the correct order. Because here, the first three parameters to the constructor are of the same type, String
, we must be very careful. If we don't get the order correct, we will not get any errors from the compiler. We might corrupt application data, and it would be hard to fix the error.
Let us now make use of the map constructor.
import groovy.transform.ToString
@ToString
class Developer {
String firstName
String lastName
String department
List<String> skills = new ArrayList<>()
}
def skills = ['Groovy', 'Java', 'JavaScript']
def map = [firstName: 'Naresha', lastName: 'Bhat', department: 'Insurance', skills: skills]
def developer = new Developer(map)
println developer
Note that I have deleted the constructor from the previous example. Hence, it's evident that Groovy, along with the no-arg constructor, creates the map constructor by default.
We can further simplify the code by using the syntax idiomatic Groovy provides.
def skills = ['Groovy', 'Java', 'JavaScript']
def developer = new Developer(firstName: 'Naresha', lastName: 'Bhat', department: 'Insurance', skills: skills)
println developer
In the above example, we did not create a map by ourselves. Instead, we passed key-value pairs as arguments to the constructor. Groovy compiler infers a map from the key-value pairs automatically.
Having understood how map-constructor works from the users' point of view, let us explore how Groovy implements this map-constructor.
In the above example, if you disassemble the generated class file, you will find that the class Developer
contains only the no-arg constructor and no other constructors.
➜ temp javap Developer
Compiled from "Script.groovy"
public class Developer implements groovy.lang.GroovyObject {
public static transient boolean __$stMC;
public Developer();
public java.lang.String toString();
protected groovy.lang.MetaClass $getStaticMetaClass();
public groovy.lang.MetaClass getMetaClass();
public void setMetaClass(groovy.lang.MetaClass);
public static java.lang.invoke.MethodHandles$Lookup $getLookup();
public java.lang.String getFirstName();
public void setFirstName(java.lang.String);
public java.lang.String getLastName();
public void setLastName(java.lang.String);
public java.lang.String getDepartment();
public void setDepartment(java.lang.String);
public java.util.List<java.lang.String> getSkills();
public void setSkills(java.util.List<java.lang.String>);
public java.lang.String super$1$toString();
}
The first time I saw this, I was intrigued by not finding the map constructor. Groovy does the following.
- The program will invoke the no-arg constructor to create an instance of
Developer
. - Then, for every argument in the constructor, the corresponding setters are invoked.
To validate this, let us add some print
statements within the default constructor and a setter.
import groovy.transform.ToString
@ToString
class Developer {
String firstName
String lastName
String department
List<String> skills = new ArrayList<>()
Developer() {
println "Default constructor invoked"
}
void setFirstName(String firstName) {
println 'Setter for firstName invoked'
this.firstName = firstName
}
}
def skills = ['Groovy', 'Java', 'JavaScript']
def developer = new Developer(firstName: 'Naresha', lastName: 'Bhat', department: 'Insurance', skills: skills)
println developer
When we run the program, it produces the following output.
Default constructor invoked
Setter for firstName invoked
com.naresha.demo.Developer(Naresha, Bhat, Insurance, [Groovy, Java, JavaScript])
With the above understanding, if you are familiar with immutable objects in Groovy, you will have an immediate follow-up question. Since immutable objects cannot contain setter methods, How does the map constructor work when the class is marked @Immutable
?
import groovy.transform.Immutable
@Immutable
class Developer {
String firstName
String lastName
String department
List<String> skills = new ArrayList<>()
}
def skills = ['Groovy', 'Java', 'JavaScript']
def developer = new Developer(firstName: 'Naresha', lastName: 'Bhat', department: 'Insurance', skills: skills)
println developer
Groovy will generate a map constructor for every class marked as @Immutable
. Let us make the class Developer
immutable and check the generated class file.
➜ temp javap Developer
Compiled from "Script.groovy"
public final class Developer implements groovy.lang.GroovyObject {
public static transient boolean __$stMC;
public Developer(java.lang.String, java.lang.String, java.lang.String, java.util.List<java.lang.String>);
public Developer(java.util.Map);
public Developer();
public java.lang.String toString();
public int hashCode();
public boolean canEqual(java.lang.Object);
public boolean equals(java.lang.Object);
protected groovy.lang.MetaClass $getStaticMetaClass();
public groovy.lang.MetaClass getMetaClass();
public void setMetaClass(groovy.lang.MetaClass);
public static java.lang.invoke.MethodHandles$Lookup $getLookup();
public java.lang.String getFirstName();
public java.lang.String getLastName();
public java.lang.String getDepartment();
public java.util.List<java.lang.String> getSkills();
public boolean super$1$equals(java.lang.Object);
public java.lang.String super$1$toString();
public int super$1$hashCode();
}
We can see the generated map constructor in the Developer
class.
If you wish to add the map constructor to a mutable class, apply the groovy.transform.MapConstructor
AST transformation to your classes.
An important question here would be why Groovy does not create a map constructor by default for mutable classes. Sometimes, we must validate the values before assigning them to the instance variables. Say, in the above example, the firstName
of the developer must be more than two characters. In a mutable class, such validations are specified in the setter methods or inside a common method and invoked from both the corresponding setter and the constructors. By making the map constructor use setter methods to initialise the instance variables, Groovy ensures the integrity of the instance variables. Also, it avoids duplicating the logic by adhering to the DRY principle.