Everybody knows that global variables are evil. In case of global constants it’s bad because the constant with the global scope creates strong dependency for a component on the data stored in this variable. If a global variable is an object, then it leads to tight coupling between client’s code and this object (strong dependency again). Java developers uses enums frequently as a container for global constants. But what if we think about it from the different point of view? Maybe it’s possible to use enums as real objects but not as a dumb constants container?

What’s wrong? Link to heading

This is an example of common Java enum which coopts all bad practives of global constants.

Assume we have CLI app where user can specify different naming policies to store some files in local file system:

  • plain names = same as default item names
  • append SHA256 checksum to file name
  • append SHA1 checksum to file name
  • other policies could be added later

This requirements I took from real practice.

// All supported naming policies
public enum NamingPolicy {
  PLAIN,
  SHA256,
  SHA1;
}

// some data item which can be saved to local file
class FileItem {

  // some data that should be saved locally
  private final Content data;

  // constructor FileItem(Content) ommited

  // save content to `dir` using `policy` for file names
  void save(Path dir, NamingPolicy policy) {
    switch (policy) {
      case PLAIN:
        Files.write(
          dir.resolve(data.name()), data.bytes()
        );
        break;
      case SHA256:
      case SHA1:
        final byte[] bytes = data.bytes();
        String hash = hex(checksum(policy.name(), bytes));
        Files.write(
          dir.resolve(hash + data.name()),
          data.bytes()
        );
        break;
      default:
        throw new UnsupportedException(
          "Unknown naming policy: " + policy
        );
    }
  }
}

The main problems with this code:

  • strong dependency on NamingPolicy constants.
  • complexity of save() method depends on amount of policies, each new policy implementation require changes in save(), this method is like a main dispatcher of all scenarios, it’s responsible for all known policy processing.
  • it’s hard to test the behavior of save(); it has many behaviors, test method should cover all possible branches to verify it. If new policy will be added but not tested, then it will be easy to see passed tests for broken code.

How to change it? Link to heading

All of the issue can be easilly fixed by replacing enum with interface with respective implementations:

interface NamingPolicy {

  // File name for given source of file and data.
  String name(String src, byte[] data);
}

// name is a given source name
class Plain implements NamingPolicy {
  @Override
  public String name(String src, byte[] data) {
    return src;
  }
}

// name is a SHA256 of content data
class Sha256 implements NamingPolicy {
  @Override
  public String name(String src, byte[] data) {
    return src + hex(checksum("SHA-256", data));
  }
}

// name is a SHA1 of content data
class Sha1 implements NamingPolicy {
  @Override
  public String name(String src, byte[] data) {
    return src + hex(checksum("SHA-1", data));
  }
}

Now FileItem class looks like this:

// some data item which can be saved to local file
class FileItem {

  // some data that should be saved locally
  private final Content data;

  // constructor FileItem(Content) ommited

  // save content to `dir` using `policy` for file names
  void save(Path dir, NamingPolicy policy) {
    Files.write(
      dir.resolve(policy.name(data.name(), data.bytes())),
      data.bytes()
    );
  }
}

All problems of global constants are solved:

  • save() methods depends on abstraction, the coupling is low
  • It’s easy to introduce new naming policy by creating an implementation of the interface. The responsibility of save() method was narrowed down to saving logic only
  • The test for save() method covers all possible scenarios, since it doesn’t depend on policies implementations

Standard enums Link to heading

But what if we move all these interface implementations to enum values? They don’t really have any state, just a naked behavior and could be organized as enum decorators:

interface NamingPolicy {

  // File name for given source of file and data.
  String name(String src, byte[] data);
}

// Appends specified digest
// of content to source name
class HashNames implements NamingPolicy {
  // digest API
  private final MessageDigest digest;

  // constructor ommited

  @Override
  public String name(String src, byte[] data) {
    final MessgeDigest copy =
      (MessageDigest) this.digest.clone();
    copy.update(data);
    return src + hex(copy.digest());
  }
}

// predefined standard policies in app domain
enum StandardPolicies implements NamingPolicy {
  // File name is a source name
  PLAIN((src, data) -> src),
  // Appends SHA1 hash of data to source name
  SHA1(new HashNames(MessageDigest.getInstance("SHA-1"))),
  // Appends SHA256 hash of data to source name
  SHA256(new HashNames(MessageDigest.getInstance("SHA-256"));

  private final NamingPolicy policy;

  StandardPolicies(final NamingPolicy policy) {
    this.policy = policy;
  }

  @Override
  public String name(final String src, final byte[] data) {
    return this.policy.name(src, data);
  }
}

So we’ve defined all standard naming policies for application, the developer can easy understand now that there are 3 standard policies in domain. Also, it’s easy to parse enum values from string, e.g. if want to pass naming policy as CLI argument, we can transform it to one of the standard policies from enum values by using StandardPolicies.valueOf(param).

Conclusion Link to heading

Java enums are not just a bag for global constants, enums can implement interfaces and behave like a real objects. It’s quite friendly to group standard objects in application domain to single enum instance. It’s easy to use but we still have enough flexibility to implement interface by other classes. So don’t afraid enums just because it’s often used wrong, use it carefully and get all benefits in your code.