Pulling out the Switch: It’s Time for a Whooping
In my previous post I talked about how if-else and switch statements are very similar in that they both ignore the problem of combining data with code.
Today I am going to show you how to refactor switch statements to alleviate that problem.
There are some varied opinions on how to refactor switch statements which I believe derive from trying to treat all switch statements as the same. I want to look at the kinds of switch statements that exist and why I recommend to refactor each one in a particular way.
Before you start learning up any new skill or concept, I suggest you take a look at my course “10 Steps to Learn Anything Quickly”.
Data to data Switch Statement
The first, most obvious kind of switch statement is one that maps one form of data to another form of data.
This example is clearly mapping one piece of data to another. The best refactor for this situation is to use a map. In C# it is a dictionary, in Java a map.
I cannot believe how many people argue against this refactoring. It doesn’t look like much, but we have greatly separated logic from data and increased the maintainability of the code.
Before our refactoring, consider, how you would be able to read all the states and capitols from a file and insert them into the switch statement? The only possible way would be through code generation. Clearly this indicates a coupling of code and data. The switch statement is formatted in such a way that it almost looks like data, but don’t let that fool you, it is code.
Consider the refactored example. If we want to read the values from a file, it is simple. So simple, that I’ll even show the code right here.
Data to action Switch Statement
This kind of switch statement appears different than data to data, but it is actually very similar. In this case we are mapping some data to a direct action that should be performed given that data.
Often this form of logic can be disguised by multiple actions happening in a case statement.
We can take the same approach here because really this is a form of mapping data to data. The second data item is essentially the name of a method to call to perform an action. We can illustrate this intent easily in C#. (In Java, you will need to wrap the action into a set of classes with a common interface.)
What we are essentially doing now is a dynamic look-up of the method to call based on the data. We could even make this example data driven from a text file that specified how to map a move to a method name or list of method names, but that is far beyond on the scope of this post, and I don’t think I would recommend it unless you have a really good reason.
Data to multiple actions Switch Statement
If you are familiar with the techniques of refactoring switch statements, you make be shaking your head by now saying, “that guy is wrong, he needs to use a factory.” Okay, well now we are going to do it.
In the data-to-action refactoring, I opted for the simplest solution that can work, instead of trying to over solve the problem by adding complexity in the form of a factory.
But, what happens when you have multiple switch statements in your code that operate on the same set of data?
Sure, we could refactor these both into maps or dictionaries. But what will happen when we try and add a new move? We’ll have to remember to add logic in both places or we’ll have a problem. In the prior example we recognized that data was being mapped to an action, so we represented that as succinctly in code as possible.
In this example, the same data is being mapped to some data that describes a move and actions to perform. We need some way to house these attributes that belong to the data we are switching on together, and we would like to have this all in one place, so we don’t have to change the code in multiple places.
Our best solution here is to use a factory that gives us the right kind of object that implements the behavior that should be tied to the data we are currently switching on.
You can see here that we are creating a factory which contains a dictionary which maps a move name to what kind of object to create. Each move implements a common IMove interface. (I only show some of the implementation here.)
Now in our code we can replace those switch statements with polymorphic behavior from our object returned from the factory.
The nice thing about this implementation is that if we try to add a new move the IMove interface will require us to implement all the proper methods. We make a change in one place and the compiler reminds us what we need to do.
Don’t jump straight to the factory
You may have heard the argument between using a factory or a dictionary to refactor switch statements before. What I am trying to show in this blog post is that it depends on your situation.
The simplest solution is a dictionary or map. Once you have a second place you are mapping the same data, you should move to a factory. The factory then contains the mapping between a piece of data and a class.
I also wanted to note here that I didn’t use enumerations. In real code you should. I avoided them here to prevent adding one more layer of abstraction so that my example would not require as much explanation.