TL;DR; Use the strategy design pattern instead of a switch statement when you don't have a small set of cases. I'll link over a YouTube video going in depth on the pattern used in this topic. Code can be obtained here: https://github.com/amainejr/csharp-designpatterns
Switch statements are another part of the programmers toolbox that can be abused if not used properly. The code I have seen written into a switch statement in the past can blow your mind, although they tend to make sense at the time. We can definitely do something about the messy situation. Let’s take a look.
Switch Statements
Starting out by a simple explanation of a switch statement is probably our best bet. A switch statement is a decision based on a set of many possible conditions. Instead of using a ton of conditions based around if statements, a switch can consolidate the decision into a pretty clean structure. Let’s take a look at an example.
// C#
int gameOutcome = -1;
switch(gameOutcome) {
case -1:
System.Console.WriteLine("Lose");
break;
case 0:
System.Console.WriteLine("Tie");
break;
case 1:
System.Console.WriteLine(“Win");
break;
default:
System.Console.WriteLine("Lose");
break;
}
Our switch has some predetermined outcomes for various possibilities of gameOutcome. In our example, when gameOutcome is equal to -1, we print out “Lose” into the console. The default keyword handles the case where nothing else matches our gameOutcome, and here we assume the player lost. Simple enough?
How can we make this better? Well, we could start by not repeating ourselves. Case -1 and the default case run the same bit of logic, so instead of needing a case for -1 and a default case, we can just use the default case.
// C#
int gameOutcome = 1;
switch(gameOutcome) {
case 0:
System.Console.WriteLine("Tie");
break;
case 1:
System.Console.WriteLine(“Win");
break;
default:
System.Console.WriteLine("Lose");
break;
}
What’s the problem with our switch statement here? Well, nothing in and of itself. Here, we’re only really ever going to need win, lose, and tie as possible outcomes. What if we have a scenario where we don’t know all of the possible cases that could be applied while we’re coding? We’d have to modify the switch every time a new case was required. That means unit tests need to be modified, code already tested by quality needs to be re-tested, etc. How can we get around this?
We don’t want to modify our class, but we still want to be able to add new cases. A design pattern will aid us in this task! Specifically, the strategy or visitor design pattern is useful for this very purpose. Let’s look at a better way to handle switches in the event that we don’t know all of the possibilities at design time.
Looking at a specific business use case scenario. You’ve got some data that you need to render on your front end website. That data contains possible sensitive information, such as an account number or social security number. You want to have some part of that data displayed, but the part of the data that is redacted can change between each piece of information. Perfect use case for the strategy pattern. Let’s look it over:
// C#
// Strategy/App/Program.cs
using System;
using Strategy.Pattern;
using Strategy.Pattern.Modifiers;
using Strategy.App;
MyData[] myData = new MyData[] {
new MyData {
content = "123-45-6789",
modifier = new ShowLastFour()
},
new MyData {
content = "123456",
modifier = new ShowMiddleTwo()
}
};
var modifier = new Modifier();
foreach(var record in myData) {
modifier.SetStrategy(record.modifier);
Console.WriteLine(modifier.Modify(record.content));
}
// Strategy/App/MyData.cs
namespace Strategy.App {
using Strategy.Pattern;
public struct MyData {
public string content;
public IModifierStrategy modifier;
}
}
// Strategy/Pattern/IModifierStrategy.cs
namespace Strategy.Pattern {
public interface IModifierStrategy {
string Modify(string data);
}
}
// Strategy/Pattern/Modifier.cs
namespace Strategy.Pattern {
using Strategy.Pattern.Modifiers;
public class Modifier {
private IModifierStrategy _strategy;
public Modifier() {
_strategy = new ShowAll(); // Defaulting to ShowAll
}
public void SetStrategy(IModifierStrategy strategy) {
_strategy= strategy;
}
public string Modify(string data) {
return this._strategy.Modify(data);
}
}
}
// Strategy/Pattern/Modifiers/ShowAll.cs
namespace Strategy.Pattern.Modifiers {
public class ShowAll : IModifierStrategy {
public string Modify(string data) {
return data;
}
}
}
// Strategy/Pattern/Modifiers/ShowLastFour.cs
namespace Strategy.Pattern.Modifiers {
public class ShowLastFour : IModifierStrategy {
public string Modify(string data) {
string redaction = new string('*',
Math.Max(0, data.Length - 4));
return redaction +
data.Substring(Math.Max(0, data.Length - 4));
}
}
}
// Strategy/Pattern/Modifiers/ShowMiddleTwo.cs
namespace Strategy.Pattern.Modifiers {
public class ShowMiddleTwo : IModifierStrategy {
public string Modify(string data) {
if (data.Length > 1) {
string redaction = "";
if(data.Length % 2 == 0) {
redaction = new string('*',
Math.Max(0, (data.Length / 2) - 1));
return redaction + data.Substring(Math.Max(0,
(data.Length / 2) - 1), 2) + redaction;
}
redaction = new string('*',
Math.Max(0, (data.Length / 2)));
return redaction + data.Substring(Math.Max(0,
data.Length / 2), 2) + redaction.Substring(1);
}
return data;
}
}
}
Sorry about the formatting there. Looks a little weird in the editor, so I had to add in line breaks for some of the longer lines. Each file has a comment prior to it's namespace declaration except for program.cs. That one is using the newer dotnet 6 code so it doesn't show all of the namespace and main program entry point.
Ok, let's now see what actually happened here and why there's not a switch statement anywhere in the code. We replaced the need for the switch statement by using the strategy pattern. The key components of the strategy pattern are the Interface (IModifierStrategy.cs), the Context (Modifier.cs) and the individual modifiers that implement the IModifierStrategy interface. For those who speak UML:

In English, what's happening is the client (Your App) has an instance of the Modifier class. The Modifier has a private instance variable of type IModifierStrategy interface. Your application then tells the Modifier which strategy it wants to use by calling the SetStrategy() method, which will update the private instance to use the concrete implementation you specified. Following that, you use the Modifier to run the Modify() method. The Modify() method will tell the private instance to run it's Modify() method, and the specific algorithm will run. Each one of the classes that implement the IModifierStrategy interface will have it's own way of going about doing the Modify() method.
Where does the strength come in from this pattern? Well, each class has only 1 reason to change. For instance, if your Modify() method of ShowLastFour needs to be updated, it doesn't have any affect on any other class within the application. Further, if a new modifier strategy is needed, for instance you want to have all characters in the string in uppercase, you simply create another class that implements the IModifierStrategy interface, and you can then tell your data to use the new strategy. This becomes exponentially useful when you have the possibility of various algorithms that are to be added over time.
Hope this helps.
Comments