Flite Careers

Wake Up From a Constructor Overload Nightmare With Java Named Params

Flite’s Java back-end uses a lot of Enums. Most of them are used for the purpose Enums were originally designed for – simple lists of settings and options. However a growing number of them are now being tasked with having each member carry a lot of metadata about its functionality.

At first this was not a big problem, but as the code evolved, more and more pieces of metadata were added to Enums with hundreds of members. Developers, not wanting to edit every single one of them to accommodate a new parameter, just added more and more constructors.

For example, this is a small part of a 100+ member Enum that defines our reporting columns

1
2
3
4
MONTH("Month", "", ColumnFormat.DATE, ColumnType.STANDARD),
TIME_ON_UNIT("Time on Unit", "Time range of user interaction.", ColumnFormat.TEXT, ColumnType.STANDARD),
TIME_ON_UNIT_ORDINAL("", "", Column.TIME_ON_UNIT, TimeOnUnit.class, ColumnFormat.NUMBER, ColumnType.ORDINAL),
INTERACTION_PERCENTAGE("Percentage (%)", "", Column.INTERACTION_COUNT, ColumnFormat.PERCENTAGE, ColumnType.FUNCTION)

Four Enum members, four different constructors – none of which are direct extensions of another. In fact, this particular Enum had eight constructors, four of which were mysteriously marked as @Deprecated. Faced with adding yet another piece of metadata to this mess, I decided to look for a better answer.

I decided to try named params. There are a number of articles and blog posts out there on the topic. The most common suggestion is using a Builder pattern, which doesn’t work when constructing an Enum. Fortunately, there’s another way.

The goal is to be able to give each Enum member an arbitrary number of options, in any order, with the simplest and easiest to read and to maintain code. To do this, you need to create four simple pieces:

1) A private inner Enum within your Enum to list your option types. This contains every type of option you will want to pass to set up your Enum. For our Column Enum, it looks like this:

1
2
3
4
5
6
7
8
private enum ColumnOptionType {
    LABEL,
    DESCRIPTION,
    FORMAT,
    TYPE,
    PARENT,
    PARENT_CLASS ;
}

2) A private static inner class within your Enum to be the common object that can stand for any option. This is ultimately what your Enum constructor takes as its varargs parameters. If you’re doing something really fancy, I could see this class having more than just two members, but in our relatively straightforward case it’s a really simple class:

1
2
3
4
5
6
7
8
9
private static class ColumnOption {
    private ColumnOptionType columnOptionType;
    private Object value;

    public ColumnOption(ColumnOptionType columnOptionType, Object value) {
        this.columnOptionType = columnOptionType;
        this.value = value;
    }
}

3) A static method for each option type. These methods create your options, and push down the actual implementation out of your Enum constructors, leaving them easier to understand. In our example they are really simple, but you could easily use these methods to do some input validation as well:

1
2
3
4
5
6
7
8
9
10
11
12
private static ColumnOption label(String name) {
    return new ColumnOption(ColumnOptionType.LABEL, name);
}
private static ColumnOption description(String desc) {
    return new ColumnOption(ColumnOptionType.DESCRIPTION, desc);
}
private static ColumnOption format(ColumnFormat format) {
    return new ColumnOption(ColumnOptionType.FORMAT, format);
}
private static ColumnOption type(ColumnType type) {
    return new ColumnOption(ColumnOptionType.TYPE, type);
}

4) A single constructor that accepts a variable number of option types. This is the piece that ties everything together into a neat package. This is where you set all of your Enum’s private fields, and where you can do validation on the parameter set as a whole – for example enforcing that type and format were specified.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
Column(ColumnOption... options) {
    Map<ColumnOptionType, Integer> map = new HashMap<ColumnOptionType, Integer>();   //used only for validations

    for (ColumnOption opt : options) {
        map.put(opt.columnOptionType, map.containsKey(opt.columnOptionType) ? map.get(opt.columnOptionType) + 1 : 1);

        switch (opt.columnOptionType) {
            case LABEL:
                this.label = (String) opt.value;
                break;
            case DESCRIPTION:
                this.description = (String) opt.value;
                break;
            case FORMAT:
                this.format = (ColumnFormat) opt.value;
                break;
            case TYPE:
                this.type = (ColumnType) opt.value;
                break;
            case PARENT:
                this.parent = (Column) opt.value;
                break;
            case PARENT_CLASS:
                this.enumClass = (Class) opt.value;
                break;
            default:
                throw new IllegalArgumentException("Unhandled columnOptionType");
        }
    }

    if (!map.containsKey(ColumnOptionType.TYPE)) {
        throw new IllegalArgumentException("Type is required for all Columns.");
    }
    if (!map.containsKey(ColumnOptionType.FORMAT)) {
        throw new IllegalArgumentException("Format is required for all Columns.");
    }

    for (Map.Entry<ColumnOptionType, Integer> entry : map.entrySet()) {
        if (entry.getValue() > 1) {
            throw new IllegalArgumentException("Column Options may only be specified once. " + entry.getKey().toString() + " occurs " + entry.getValue() + " times.");
        }
    }
}

So what does all of this code buy you? After a little refactoring, your Enum now looks like this:

1
2
3
4
MONTH(label("Month"), format(ColumnFormat.DATE), type(ColumnType.STANDARD)),
TIME_ON_UNIT(label("Time on Unit"), description("Time range of user interaction."), format(ColumnFormat.TEXT), type(ColumnType.STANDARD)),
TIME_ON_UNIT_ORDINAL(parent(Column.TIME_ON_UNIT), parentClass(TimeOnUnit.class), format(ColumnFormat.NUMBER), type(ColumnType.ORDINAL)),
INTERACTION_PERCENTAGE(label("Percentage (%)"), parent(Column.INTERACTION_COUNT), format(ColumnFormat.PERCENTAGE), type(ColumnType.FUNCTION))

A little longer than the original, but now far more readable. No more trying to figure out just which constructor is being used, what the 5th String parameter means, but only if the 4th parameter is a Long, and so on.

Best of all – adding a new option type requires you to add small pieces to the four components outlined above – surely a non-zero amount of work, but far simpler than refactoring a giant Enum to update each member’s constructor or descending even deeper into overload hell by creating yet another constructor with its own set of parameters.