August 16, 2015

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



Even with the new fancy additions to Java 8, enums stay one of the most useful – and most underestimated – features of the language. When applied correctly, they can increase code maintainability considerably. In this article, I’d like to present some common best practices which stem from my Java EE experience.

Enumify your constants!

First of all, it’s crucial to understand in which situation usage of enums is appropriate. I consider the following to be a general rule for Java software maintainability:

For case differentiations, constant fields are dead. Always use enums instead!

Consider the following implementation, making use of “classic” constant fields to distinguish some “file format”:
public static final int FORMAT_PDF = 0;
public static final int FORMAT_EXCEL = 1;
public static final int FORMAT_CSV = 2;

public static void formatFile(File file, int format) {
    // do something based on format
}
Compare this with an implementation which uses an enum to encapsulate the supported file format types:
public static enum Format {
    PDF, EXCEL, CSV;
}

public static void formatFile(File file, Format format) {
    // do something based on format
}
Just by examining the code which uses enums, the developer can say with certaintly what are valid values of the formatFile(…)’s format parameter. And, even more importantly, so can the compiler!

Enums shift information from runtime to compile time. They make a program crash early, and that’s a good thing. In the example, you just cannot compile formatFile(…) with an unknown, undefined format parameter (except null). To sum up:

Having information available at compile time means, that it is available:
  • For the compiler
  • For you, as the developer, by just looking at the code
Having information available at runtime means that:
  • It’s not available for the compiler. It can only be detected at runtime, requiring you to write additional tests.
  • It’s available for you, as the developer, only when you run the program (e.g. in a unit test) and debug it.
Java is a statically typed language and as such, it has a compiler which can statically check syntax constraints. This comes at a high price of flexibility, when compared with dynamically typed languages without compiler (such as Ruby). Let’s not waste this investment! Make use of compile time checking whenever possible to increase code robustness and maintainability. That’s the main lession learnt from proper enum usage.

As you can see, it never makes sense to use constant fields for case differentiations, they can and should always be replaced by enums. Still, having constant fields is a quite common anti-pattern. It doesn’t really help that unfortunately, it’s even quite widespread in some of Java’s core classes, e.g. in the old Swing classes. They’re actually quite good illustrations of the anti-pattern: Unless you’re lucky and the constant fields are declared in the same class as is the receiver of the constant, you just have no chance to find out what are valid constant values just by looking at the API. They truly are the implementation of the „magic number“ anti-pattern.

Enums are objects – strategy pattern on enum

Let’s again examine above example code we have just deemed “best practice”:
public static enum Format {
    PDF, EXCEL, CSV;
}

public static void formatFile(File file, Format format) {
    // do something based on format
}
Even though it’s now compile-time assured that the format parameter is within the set of valid parameter values, by taking a closer look at the method implementation, it is still not assured that it matches the actual value because implementation (inside formatFile(…)) and case differentiation (enum type definition) live independently of each other. This is a violation of the “Open / closed principle”: A change in one place (e.g. adding a new format) requires a change in another place (e.g. the formatFile(…) implementation. We have basically just shifted from a magic number to a magic enum.

This inconsistency exists because the code does not adhere to object oriented principles: As the Format enum is an object, it should be responsible for its own behavior. The formatFile(…) method thus belongs inside the Format enum!

There are actually three ways to achieve this. The most simple and naïve way is to implement a central method on the enum:
public enum Format {
    PDF, EXCEL, CSV;
    
    public void formatFile(File file) {
        switch (this) {
            case PDF:
                System.out.println("PDF format");
                return;
            case EXCEL:
                System.out.println("PDF format");
                return;
            case CSV:
                System.out.println("PDF format");
                return;
            default:
                throw new IllegalStateException("Format not recognized: " + this);
        }
    }
}
Actually, this is still unsafe in the very same way as the previous implementation, but at least, the logic now resides in the same (enum) class which also defines the case differentiation. For a developer, it’s thus more likely that he remembers e.g. to add another switch case branch to that method if he adds an additional format enum value. Also note that the default switch branch throws an exception. This is extremely helpful to make sure that even if an error only shows at runtime, it shows immediately.

Because enums support inheritance as well, you can make the individual values implement a common interface:
public 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");
        }
    };
    
    public abstract void formatFile(File file);
}
This is fully object-oriented compliant, but it’s arguably also hard to read; even more so, if there were multiple methods implemented by the enum!

As a variation of this previous approach, you could delegate implementation to a separate class which is then wrapped by the enum:
public enum Format {
    PDF(new PdfFormat()),
    EXCEL(new ExcelFormat()),
    CSV(new CsvFormat());
    
    private final BaseFormat format;

    private Format(BaseFormat format) {
        this.format = format;
    }
    
    public void formatFile(File file) {
        format.formatFile(file);
    }
}

public abstract class BaseFormat {
    public abstract void formatFile(File file);
}

public class PdfFormat extends BaseFormat {
    @Override
    public void formatFile(File file) {
        System.out.println("PDF format");
    }
}

public class ExcelFormat extends BaseFormat {
    @Override
    public void formatFile(File file) {
        System.out.println("Excel format");
    }
}

public class CsvFormat extends BaseFormat {
    @Override
    public void formatFile(File file) {
        System.out.println("CSV format");
    }
}
This introduces more boilerplate code to write the individual classes, but it’s far more readably, plus you can split the individual implementations across multiple files to further increase readability. It's a bit less safe than the implementation on the enum value itself though.

To be honest, for very simple cases, I use the most simple version although not perfectly “secure”. For more complex cases (5+ enum values or 2+ enum methods), I use the latter approach.

Pages: 1 2 3