In Java 1.5 enums were introduced to ease handling of constant values. Enums can improve code quality, because they are types that the compiler can check. But they can also be misused and lower the code quality. In this blog I want to discuss the misuse of enums as type discriminators and show you why this kind of usage is an anti-pattern from an object-oriented perspective.

Type-switches

What I call a “type-switch” is just any conditional statement that does type checks in order to execute type specific code. Here are some examples of type switches.

In this blog I want to focus on enums as type switches, but the basics behind it will apply to any type-switch.

Enum as type discriminator

In some tutorials and even production code you will see enum types that are used as type-switches.  The example code I want to use here is taken from another blog “Of Hacking Enums and Modifying final static Fields“by Dr. Heinz M. Kabutz. This blog is about testing issues that arise because of the type-switch usage of enums and how to solve them.

Let’s assume you want to design a human beings and let they sing depedent on their state. Some developers might want to do it this way:

Problems with this design:

  1. single responsibility principle is violated
    The single responsibility principle says that there should only be one reason to change. But the method sing() will change whenever a HumanState is added or removed.
  2. cyclomatic complexity increases fast
    Cyclomatic complexity is a software metric that indicates the complexity of a program. It is a quantitative measure of the number of linearly independent paths through the source code. Therefore the complexity of the method sing() increases with every new human state .
  3. Hard unit-testing and code coverage
    In order to test the complete behavior of the sing() method one must cause the switch statement to fall through to the default case. But this means that one must mock the enum HumanState, because the normal “production” code doesn’t define an illegal value for a human state. Take a look at “Of Hacking Enums and Modifying final static Fields” to see the effort you have to make to get the default case tested.
  4. Default case handling
    Most times developers add a default case to be remembered when a new enum value is added and to prevent unexpected behavior. Thus you must also implement a default case even if it is never executed until the enum values change.

 

Type-Switches in an anemic design

An anemic design inverses the object-oriented way. Instead of calling methods on objects (data structures) you pass data structures into methods.

In this king of design you often have to deal with type-switches as well. E.g.

In a real world application there will be many services that take an order object and you will often see that a lot of services do type checks or in other words… execute logic dependent on some sort of type discriminator.

This leads to the problem that the type-switch will be distributed all over the code base. As a result of this it is hard to estimate the impact of a type-switch change.

An object-oriented way eliminates type-switches

Let us remember the first example of the design of  human beings and their state management. If we analyse the code above we will recognize that the human state behavior depends on a state and we can desing the human state as an own class or better an interface.

Now we can move the sing() logic into dedicated implementations. E.g.

After we have done this the Human class will become much easier.

The difference

  1. single responsibility principle is respected
  2. cyclomatic complexity for the sing method is 1 … so the implementations of HumanState
  3. Every state can be independently unit tested.
  4. There is no default case anymore. So we don’t have to test it.

Type-switches and the factory method pattern

Also the factory pattern often suffers from type-switches. The next example is taken from a stackoverflow question.

 

You can also eliminate this type switch by introducing an interface and using a map.

You might also want to implement a default constructor factory for the simplest case.

Conclusion

If you eliminate type-switches you will benefit from

  • single responsibility
  • easy unit testability
  • less cyclomatic complexity
  • eventually eliminate default case handling

Recommended Reading