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.

  1. The program will invoke the no-arg constructor to create an instance of Developer.
  2. 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.