August 16, 2015

Java enums best practices by example (part 2 of 3)


Pages: 1 2 3

Enumify your input with enum reverse lookup

Making constants compile-time safe makes sense because they never change at runtime. But enums can and should also be applied to make case differentiations at runtime.

Let’s assume that we receive the file format at runtime through user input, which is a String. A naïve implementation would be:
public static void formatFile(File file, String format) {
    switch (format) {
        case "pdf":
            System.out.println("PDF format");
            return;
        case "excel":
            System.out.println("Excel format");
            return;
        case "cvs":
            System.out.println("CVS format");
            return;
        default:
            throw new IllegalStateException("Format not recognized: " + format);
    }
}
Even with Java 8’s new support for switch on String (rather than if / else ifs), this implementation is brittle. This is even more true if input is processed through multiple steps (a.k.a. method calls) in the same original form, infecting the whole API with a non-compile time safe data type. In case of a String being passed around, this anti-pattern is famously known as “Stringly typed” programming (as a pun on opposite “strongly typed”).

What you want to do is sanitize so-called tainted input. You want to do this as early as possible in your call chain and you want to make it, if possible, compile-type safe. Thus, enums are a perfect choice.

To turn any kind of runtime-determined data into an enum, the enum reverse lookup pattern is a known best-practice. Consider this implementation:
public static enum Format {        
    PDF {
        @Override
        public void formatFile(File file) {
            System.out.println("PDF format");
        }
    }, EXCEL {
        @Override
        public void formatFile(File file) {
            System.out.println("Excel format");
        }
    }, CSV {
        @Override
        public void formatFile(File file) {
            System.out.println("CSV format");
        }
    };
    
    private static final Map<String, Format> LOOKUP;
    
    static {
        Map<String, Format> map = new HashMap<>();
        map.put("pdf", PDF);
        map.put("excel", EXCEL);
        map.put("cvs", CSV);
        map = Collections.unmodifiableMap(map);
        LOOKUP = map;
    }
    
    public static Format getFrom(String input) {
        Format ret = LOOKUP.get(input);
        if (ret == null) {
            throw new IllegalArgumentException("Not a valid format: " + input);
        }
        return ret;
    }
    
    public abstract void formatFile(File file);
}

public static void formatFile(File file, String format) {
    Format.getFrom(format).formatFile(file);
}
Here, the mapping from user input to a compile time checked enum (the sanitizing) happens in one place, namely through a map-based lookup (the map can / should be final and immutable). This is highly performant, maintainable and testable. Remember to check for an invalid lookup in the static lookup function.

In the example, the solution is combined with previous best practice to use enum inheritance. Note that the client code (method formatFile(…)) is free of any input sanitizing / differentiation.

If that method would invoke another method, you would then of course use the sanitized, compile-time safe enum value in any subsequent API call.

…or with the strategy pattern on the input

Of course, if the input type is not a primitive or a Java core / third party type but a class you own yourself, you would use the object oriented strategy pattern to turn it into an enum rather than a static helper reverse lookup.

Let’s assume that in the next example, MyFile is loaded from a DB and we want to extract the file format information and make it compile time safe.
public static abstract class MyFile {}

public static class PdfFile extends MyFile {}

public static class ExcelFile extends MyFile {}

public static class CsvFile extends MyFile {}

public static enum Format {
    PDF, EXCEL, CSV;
}

public static void formatFile(MyFile file) {
    // use a helper method to extract Format from MyFile
}
Here, using enum reverse lookup or any other static helper method is not desirable because again, the Open / closed principle is violated as adding a new MyFormat subtype would imply a change to the lookup function.

Instead, use the strategy pattern with inheritance on the type itself:
public static abstract class MyFile {
    public abstract Format getFormat();
}

public static class PdfFile extends MyFile {
    @Override
    public Format getFormat() {
        return Format.PDF;
    }
}

public static class ExcelFile extends MyFile {
    @Override
    public Format getFormat() {
        return Format.EXCEL;
    }
}

public static class CsvFile extends MyFile {
    @Override
    public Format getFormat() {
        return Format.CSV;
    }
}

public static enum Format {
    PDF, EXCEL, CSV;
}

public static void formatFile(MyFile file) {
    Format format = file.getFormat();
}
Because the format transformation is defined abstractly on the base class, the developer is literally forced to provide a sensible file format for every new subclass. There’s no central, remote case differentiation anymore.

(By the way, the example of course is quite artificial and suspicious. You would rather just define formatFile() as a member method of MyFile and get rid of the enum altogether.)

Pages: 1 2 3