Use chains, not switches

An alternative to the common Java structure

Use chains, not switches

The Switch statement is a well-known construct in many programming languages. In many Java projects it is used in conjunction with enums to execute statements. Enums are collections of constant values. Here are some examples.

public enum ListItemMarker {
  CIRCLE, SQUARE, DIAMOND;
}

public enum DesignPattern {
  SINGLETON, CHAIN_OF_RESPONSIBILITY, STRATEGY,
  FACADE, ADAPTER, BRIDGE, BUILDER, VISITOR,
  PROTOTYPE, FACTORY, STATE;
}

Let's have a look at how we can use the ListItemMarker enum together with a switch statement to compute a String representation of the marker.

public String asString(ListItemMarker marker) {
  switch(marker) {
    case CIRCLE: return "⬤";
    case SQUARE: return "■";
    case DIAMOND: return "◆";
    default: return "";
  }
}

Note that this example is so simple, the marker Strings could well be properties of the enum itself. Each constant would then know the correct value at construction. For the purpose of this blog however, we want to use a switch statement.

Time to get serious

Small enums turn your switch statement into a small block of code. This has several benefits:

  • you can see which constant leads to which marker with one eye;
  • it's not a lot of code, and still quite readable,
  • it's quite easy to test.

Let's compare this to a switch statement for a larger Enum, e.g. DesignPattern.

public class Example {
  public boolean contains(String text, DesignPattern pattern) {
    switch(color) {
      case SINGLETON: return text.contains("singleton");
      case CHAIN_OF_RESPONSIBLITY: return text.contains("chain");
      case STRATEGY: return text.contains "strat");
      case FACADE: return text.contains("facade");
      case ADAPTER: return text.contains("adaptrr");
      case BRIDGE: return text.contains("golden gate");
      case VISITOR: return true;
      case PROTOTYPE: return text.contains("proto") 
                              && text.contains("type");
      case FACTORY: return (new TextFactory(text)).containsFactory();
      case STATE: return text.contains("state");
      default: return false;
    }
  }
}

Most code in large production projects will start looking like this. The enums are no longer small and the switch statements are hardly readable. The tests for a method like this can be parameterized thanks to JUnit but that does not help in documenting what the method is doing. While the code itself is still easy to test, it's getting harder to read and understand the test, let alone the production code itself.

Did you spot the ADAPTRR bug in the example above? Are the other cases correctly implemented? We don't really know. Maybe the person who implemented it knows. But will that person know after six months, or two years? I doubt it.

Looking at this code from a different angle, is it the responsibility of this class to know all these different calculations? Chances are slim.

Now what?

What we are seeing here are different ways to compute something, based on an enum value. Which design pattern allows us to use different computational methods? Right, the Strategy pattern!

We can create Strategy classes for each enum value to make all these computations readable and testable as follows:

public interface DesignPatternContainsStrategy {
  boolean contains(String text);
}

public class SingletonContainsStrategy
  implements DesignPatternContainsStrategy {
  @Override
  public boolean contains(String text) {
    return text.contains("singleton");
  }
}

By creating an interface, we have a contract for the specific classes to follow. We can clean up the Example class now!

public class Example {
  public boolean contains(String text, DesignPattern pattern) {
    switch(color) {
      case SINGLETON: 
        return new SingletonContainsStrategy().contains(text);
      case CHAIN_OF_RESPONSIBLITY: 
        return new ChainOfResponsibilityContainsStrategy().contains(text);
      case STRATEGY: 
        return new StrategyContainsStrategy().contains(text);
      case FACADE: 
        return new FacadeContainsStrategy().contains(text);
      case ADAPTER: 
        return new FacadeContainsStrategy().contains(text);
      case BRIDGE: 
        return new BridgeContainsStrategy().contains(text);
      case VISITOR: 
        return new VisitorContainsStrategy().contains(text);
      case PROTOTYPE:         
        return new PrototypeContainsStrategy().contains(text);
      case FACTORY: 
        return new FactoryContainsStrategy().contains(text);
      case STATE: 
        return new StateContainsStrategy().contains(text);
      default: return false;
    }
  }
}

The sole purpose of this class is now to decide which strategy to use. Much better, don't you agree? By the way, did you happen to spot the ADAPTER bug? Looks like we fixed the Single Responsibility problem but not the readability problem. To actually make our code more robust and more readable, let's ditch the switch statement entirely and go for an additional design pattern.

Chains, chains, chains

Let's mix up our Strategy with a Chain of Responsibility. Every implementation of the DesignPatternContainsStrategy will have an accept method to indicate to which enum value it belongs.

public interface DesignPatternContainsStrategy {
  boolean accepts(DesignPattern pattern);
  boolean contains(String text);
}

public class SingletonContainsStrategy
  implements DesignPatternContainsStrategy {
  @Override
  public boolean accepts(DesignPattern pattern) {
    return DesignPattern.SINGLETON == pattern;
  }

  @Override
  public boolean contains(String text) {
    return text.contains("singleton");
  }
}

An additional benefit of such chains is that much like switches, they allow us to use the same strategy for multiple enum values. By implementing your accepts method to check whether the given enum value is in a collection, you can easily reuse the same strategy for multiple enum values. However, don't walk into the DRY trap. Single responsibility is key here. If your accepts method becomes overly complex, you're doing it wrong. Keep it simple!

Let's use the chain in our Example class by passing the strategies in the constructor. In your own codebase, it is your choice to create the strategies as POJOs in a non-argument constructor or to have them injected with a dependency-injection framework like Spring. Whichever works for you, as long as it's consistent with your codebase.

public class Example {

  private List<DesignPatternStrategy> strategies;

  public Example(List<DesignPatternStrategy> strategies) {
    this.strategies = strategies;
  }

  public boolean contains(String text, DesignPattern pattern) {
    return strategies.stream()
      .filter(strategy -> strategy.accepts(pattern))
      .findFirst()
      .map(strategy -> strategy.contains(text))
      .orElse(false);
  }
}

Some pros and cons

The classes above are all quite small and very readable. Even if one strategy becomes utterly complex, this does not pollute the other strategies. If you need to delete a strategy, you can just delete its class (and where it is created, if that is necessary). By naming your strategies wisely you will find them fast in your IDE. Imagine having to locate the correct case statement in a 500+ line switch. They exist, and they are not fun to work with.

You can test each strategy as well as the Example individually, with small and readable tests. Integration testing the entire Example is also possible. You can still use Parameterized JUnit tests, especially when verifying the accept methods.

Will you have less bugs with this approach? Maybe. Maybe not. One of the advantages of these strategies is that if one of your strategies contains a bug, fixing that will not impact other strategies at all. While in a large switch case, you might make fix one case and accidentally create a bug on a case below. It sounds stupid but it comes down to this: if you're in the same method, you're touching the same algorithm. This is no longer the case when splitting up the Switch statement into different strategies.

You can still create non-readable code by making overly complex accept methods, or implement your strategies so that they have multiple responsibilities. In the end, using patterns does not make good code or eliminate bugs. Common sense, simplicity and readability do.

The end of the Switch statement... or is it?

Please don't get me wrong. This is not a blog post to ditch switches altogether. In fact, I applaud Java 13 for bringing Switch Expressions. Small switches have their use and the expressions allow us to write more concise and readable code.

My rule of thumb is the following. If I look at a switch statement and I have to think about what's going on, the statement needs to go. If a switch uses private methods for all its cases, it's already trying to be a list of strategies. Might as well refactor it to make it easier to read and test. Last but not least: even if the cases are simple but there's a lot of cases, use common sense. Don't be a developer who maintains switch statements consisting of 100+ lines of code. Refactor them instead.