Eliminating Cylclomatic Complexity by replacing switch/case with a method or a Dictionary<TKey, func<>>
Cyclomatic Complexity is a measurement of how many paths your code could traverse. Switch/case statements are often immediate Cyclomatic Complexity concerns.
Cyclomatic Complexity Example
Imagine the following code:
public void Foo(int val) { switch (val) { case 0: // ... code here break; case 1: // ... code here break; case 2: // ... code here break; case 3: // ... code here break; case 4: // ... code here break; case 5: // ... code here break; case 6: // ... code here break; case 7: // ... code here break; } }
In the above code, there are 8 paths. The Cyclomatic Complexity is not small. This makes unit tests difficult. It is complexity that is unnecessary. Unnecessary complexity leads to bugs.
Replacing a switch/case statement
Almost invariably, the switch/case statement can be replaced in a way that removes the cyclomatic complexity.
Note: While this article is about C#, it might be helpful to note that Python didn’t even implement a switch/case statement.
There are three replacements methods I am going to dicuss here.
- Replace with a Method
- Replace with a Method and a dictionary provided parameter.
- Replace with Dictionary<TKey, Func<>>
To know which one to choose, you have analyze the code. In the above example, I left out the code. I just put a place holder for it.
// ... code here
After analyzing the code, you should be able to pick one of the following:
Method
So you should use a method if you can.
Example 1
Imagine the following snippet. This is an easy one. You should pick out the replacement without having to be told what it is.
public void Foo(int val) { switch (val) { case 0: Bar.Do(0); break; case 1: Bar.Do(1); break; case 2: Bar.Do(2); break; case 3: Bar.Do(3); break; // , ... , case 7: Bar.Do(7); break; } }
As you can see here, each method is easily following a pattern. We can replace the switch statement with a single method call.
public void Foo(int val) { Bar.Do(val); }
Look, that one was obvious and it was intended to be obvious. It isn’t always going to be obvious.
Example 2
Imagine the following snippet. This is also an easy one, but not quite as easy as above. Hopefully, you pick out the replacement without having to be told what it is.
public void Foo(int val) { switch (val) { case 0: Bar.Do0(); break; case 1: Bar.Do1(); break; case 2: Bar.Do2(); break; case 3: Bar.Do3(); break; // , ... , case 7: Bar.Do7(); break; } }
Notice there is a pattern. We know the method name to call on Bar because we can see the pattern: “Do” + val
We could easily use reflection to eliminate cyclomatic complexity here.
Note: While reflection is often deemed slow and a cause of performance issues, in practice, unless looping through large data sets, any performance loss from reflection is not measurable.
public void Foo(int val) { typeof(Bar).GetMethod("Do" + val).Invoke(); }
We traded Cyclomatic Complexity for Reflection and a possible, but unlikely performance issue. If this code is used in a loop for millions of instances in a data set, you might not want to do this.
Method and a dictionary provided parameter
Example 1
Imagine the code is more like this, in which different case statements call different overloaded values.
public void Foo(int val, ObjA a) { switch (val) { case 0: Bar.Do(a, 3); break; case 1: Bar.Do(a, 7); break; case 2: Bar.Do(a, 5); break; case 3: Bar.Do(a, 100); break; case 4: Bar.Do(a, 9); break; case 5: Bar.Do(a, 12); break; case 6: Bar.Do(a, -1); break; case 7: Bar.Do(a, int.MaxValue); break; } }
So every case statement is doing something different. However, notice that what it does differently is a static int. We can create a static parameter dictionary of type Dictionaryint, int>.
internal Dictionary<int, int> ParamMap = new Dictionary<int, int> { {0,3}, {1,7}, {2,5}, {3,100}, {4,9}, {5,12}, {6,-1}, {7, int.MaxValue } }; public void Foo(int val, ObjA a) { Bar.Do(a, ParamMap[val]); }
This uses a static, prebuilt dictionary that completely eliminates Cyclomatic Complexity.
Notice all the Cyclomatic Complexity is gone. This code never branches. There is very little left to test.
Example 2
Imagine the code is more like this, in which different case statements call different overloaded values.
public void Foo(int val, ObjA a, ObjB b, ObjC c) { switch (val) { case 0: Bar.Do(a); break; case 1: Bar.Do(b); break; case 2: Bar.Do(c); break; case 3: Bar.Do(a, c); break; case 4: Bar.Do(b, c); break; case 5: Bar.Do(b, c, a); break; case 6: Bar.Do(b, c, a * .01); break; case 7: Bar.Do(a, b, c); break; } }
This looks harder doesn’t it. The Cyclomatic Complexity can still be simplified. How are we going to do it?
Well, one option is to use a Dictionary<int, object[]>.
public void Foo(int val, ObjA a, ObjB b, ObjC c) { var Dictionary<int, object[]> paramMap = new Dictionary<int, object[]>(); paramMap.Add(0, new []{ a }); paramMap.Add(1, new []{ b }); paramMap.Add(2, new []{ c }); paramMap.Add(3, new []{ a, c }); paramMap.Add(4, new []{ b, c }); paramMap.Add(5, new []{ b, c, a }); paramMap.Add(6, new []{ b, c, a * .01 }); paramMap.Add(7, new []{ a, b, c }); typeof(Bar).GetMethod("Do").Invoke(paramMap[val]); // Reflection allows for passing in a dynamically sized list of parameters. }
The solution is almost exactly the same as above. The differences are:
- The dictionary is dynamic, based on the passed in parameters, so we have to build it dynamically.
- The parameters are dynamic so we call the method with reflection to allow for dynamic parameters.
The dictionary still completely eliminates Cyclomatic Complexity. Notice all the Cyclomatic Complexity is gone. This code never branches. There is very little to test.
There is the overhead of creating a Dictionary and the overhead of reflection, but again, unless you plan to use this for looping through large data sets, the performance difference is negligible.
Dictionary<TKey, Func<>>
Sometimes there isn’t much common at all. Sometimes, the complexities very greatly.
public void Foo(int val, Obj a) { switch (val) { case 0: // ... code goes here break; .... } }
Imagine the code in the “code goes here” section is vastly different. Imagine you just can’t find much common ground. In these situations, you can use Dictionary<TKey, Func<>>. The pattern is to put the dictionary in its own class file. Then the object that uses it can have an injectable IDictionary<TKey, Func<>>. Injection options are: Constructor injection, Method injection, property injection. I lean toward a property injection variation called a Lazy Injectable Property.
Question: What generic paramaters should be used for the Dictionary?
Answer: The TKey is clearly the type of the val property, which in the example is an int.
Question: What generic parameters should be used for the Func<>?
Answer: Well, you need to think through to get this answer. First, you need to find the Lowest Common Parameter Set. Second you need to check the return type.
Finding the Lowest Common Parameter Set
If you look at one of the above methods, you can easily get the lowest common parameter set by writing down each and every parameter pass in. Remember this method from above in Example 2?
public void Foo(int val, ObjA a, ObjB b, ObjC c) { // Switch/case statement here . . . }
The lowest common parameter set is: a, b, c. If you look at the full implementation further up, you will notice that none of the methods take in val, so val is not included in the parameter set as it is the Dictionary’s key.
So now we can create our Dictionary. We will have three input parameters.
Note: Not all variables are passed in. Some may be local to the class or method.
Action<> vs Func<>
This is easy. The only notable difference is that Action<> takes in parameters and returns void. Func<> takes in parameters and returns the type specified in the last generic type.
So as there is no return value in the above example, we can use this code:
public Class FuncDictionary : Dictionary<int, Action<ObjA, ObjB, ObjC>> { public FuncDictionary() { this.Add(0, (a, b, c) => { Bar.Do(a); } ); // Parameters b, c are ignored. That is ok. this.Add(1, (a, b, c) => { Bar.Do(b); } ); this.Add(2, (a, b, c) => { Bar.Do(c); } ); this.Add(3, (a, b, c) => { Bar.Do(a, c); } ); this.Add(4, (a, b, c) => { Bar.Do(b, c); } ); this.Add(5, (a, b, c) => { Bar.Do(b, c, a); } ); this.Add(6, (a, b, c) => { Bar.Do(b, c, a * .01); } ); this.Add(7, (a, b, c) => { Bar.Do(a, b, c); } ); } }
Now look at the foo code.
// Lazy injectable property internal IDictionary<int, Action<ObjA, ObjB, Objc> ActionDictionary { get { return _ActionDictionary ?? (_ActionDictionary = new FuncDictionary()); } set { _ActionDictionary = value; } } private IDictionary<int, Action<ObjA, ObjB, Objc> _ActionDictionary; public void Foo(int val, ObjA a, ObjB b, ObjC c) { ActionDictionary[val].Invoke(a, b, c); }
In all the previous methods, we resolved Cyclomatic Complexity by taking a method with 8 branches, and reducing that 1 method to 0 branches. We can also get 100% code coverage with 1 unit test.
1, Methods
0, Cyclomatic Complexity
In this final Dictionary<TKey, Func<>> example, we end up with 8 methods that need testing.
8, Methods
0, Cyclomatic Complexity each
We still have to test all 9 methods (8 funcs in in the FuncDictionary and the original method). However, when that work was in the switch/case statement, that code would be harder to isolate for unit tests. With the refactor to Dictionary<TKey, Func<>>, all eight methods are isolated and unit tests are simplified. The single responsibility is followed. The code is simply S.O.L.I.D. You could even inject interfaces with Dependency Injection that provide those methods. The Switch/Case statement appeared easier to write, but it usually leads to more code coupling, makes code harder to unit test and maintain; not to mention adds difficulty to future edits or replacing code, or the difficulty of dependency injection.
By the way, if the code were extremely simplistic, with only three or four cases, then it might be fine to use the switch/case statement. But as you start seeing your code get complex inside each case statement, then this Dictionary option is for you.
If the code is never going to change and will have only a few switch statements, then it might be fine to use the switch/case statement.
If you are going to continually add more cases, then this Dictionary option is for you. Think about how using a Dictionary now enables you to more fully embrace the O in SOLID, as it is impossible to make a Switch/Case statement code closed for modification but open for extension. However, it is simple to make the Dictionary version closed for modification but open for extension.