Wednesday, February 13, 2019

Featured enum instead of switch

Problem and  its solution

Switch/case is the common control structure implemented in most imperative programming languages. Switch is considered more readable than series of if/else.

Here is a simple example:
// Switch with int literal
switch (c) {
  case 1: one(); break;
  case 2: two(); break;
  case 3: three(); break;
  default: throw new UnsupportedOperationException(String.format("Operation %d is not supported", c));
}


Here is the list of the main problems in this code:

  1. Relationship between int literals (1, 2, 3) and executed code is not obvious.
  2. If one of the values (e.g. 2) becomes not supported anymore and this switch is not updated accordingly it will contain  forever the unused code.
  3. If new possible value of c (e.g. 4) is introduced and the switch is not updated accordingly the code will probably throw UnsupportedOperationException at runtime without any compile time notifications.
  4. Such switch structure tends to be duplicated several times in code that makes problems 2 and 3 even more complicated. 
The first simplest fix can be done by using int constants instead of literals. First, let's define constants:

private static int ONE = 1;
private static int TWO = 2;
private static int THREE = 3;

Now the code will look like this:

switch (c) {
  case ONE: one(); break;
  case TWO: two(); break;
  case THREE: three(); break;
  default: throw new UnsupportedOperationException(String.format("Operation %d is not supported", c));
}


(Obviously in real life the names of the constants must be self descriptive)
This snippet is more readable but all other disadvantages are still relevant. The next attempt to improve the initial code snippet uses enums introduced to Java language in version 5 in 2004. Let's define the following enum:

enum Action {ONE, TWO, THREE}


Now the switch snippet will be slightly changed:

Action a = ...
switch (a) {
  case ONE: one(); break;
  case TWO: two(); break;
  case THREE: three(); break;
  default: throw new UnsupportedOperationException(String.format("Operation %s is not supported", a));
}


This code is a little bit better: it will produce compilation error if one of the elements is removed from enum Action. However, it will not cause compilation error if additional element is added to enum Action. Some IDEs or static code analysis tools may produce warning in this case, but who is paying attention to warnings? Fortunately enum can declare abstract method that has to be implemented by each element:


enum Action {
  ONE { @Override public void action() { } }, 
  TWO { @Override public void action() { } }, 
  THREE { @Override public void action() { } }, 
  public abstract void action();
}


Now the switch statement can be replaced by single line:


Action a = ...
a.action();


This solution does not have any of disadvantages enumerated above:

  1. It is readable. The method is "attached" to enum element; one can write as many javadoc as it is needed if method meaning is unclear. The code that calls method is trivial: what can be simpler than method invocation?
  2. There is no way to remove enum constant without removing the implementation, so no unused code will remain if some functionality is no longer relevant.
  3. New enum element cannot be added without implementation of method action(). Code without implementation can't be compiled. 
  4. If several actions are required they all can be implemented in enum. As we already mentioned the code that calls specific function is trivial, so now there is no code duplication. 

Conclusion

Although switch/case structure is well known and widely used in various programming languages its usage may cause a lot of problems. Solution that uses  java enums and described above does not have these disadvantages. The next article from this series shows how to extend functionality of existing enum.

3 comments:

  1. Basically what you use is polymorphism, except you didn't mention how to come from condition defined initially as 'c' to new action enum expressed as 'a'. And this is a place where often this conversion is done by switch/case.

    ReplyDelete
    Replies
    1. Piotr, thanks for your comment. You are right, technically I am using polymorphism. In fact, this is the basic OOD brick. Concerning to the a variable. You get it exactly as you get the c variablein switch/case solution. It does not matter how do you get it. The idea is to avoid switch/case and I did my best to explain why.

      Delete
    2. But you aren't really avoiding switch...

      Delete