3 Reasons Why The switch Statement Is Killing Our Software
When I first started programming, I fell in love—with the switch
statement. Here I had found a way to construct long if ... elseif ... elseif ... else
conditionals in a slightly more concise way. I was in programming heaven.
The switch
statement
The switch
statement exists in many programming languages (e.g. C, C++, C#, Java, JavaScript, etc.). It represents a control flow mechanism that maps the value of a variable or expression onto the selection and execution of different code blocks described by case
statements.
An example of a switch
statement:
switch (paymentGateway) { case "Paypal": // Pay with Paypal. // ... code to pay using PayPal payment gateway. break; case "Stripe": // Pay with Stripe. // ... code to pay using Stripe payment gateway. break; default: throw new UnknownPaymentGateway(paymentGateway); }
Here, the switch
statement selects and calls the code to pay for an owed amount using one of two payment gateway options. When the value of reference paymentGateway
, matches the string literal within a case
statement, say “Paypal”, we execute the code block immediately below the case
statement—up to the following break
statement. On the other hand, when paymentGateway
contains a string other than “Paypal” or “Stripe”, the code below the default
statement is executed and, in this instance, throws an UnknownPaymentGateway
exception.
There you go; if you didn’t know it already, you’ve just learned the basic usage of a switch
statement. Since you’re reading an article about staying clear of switch
statements due to the damage they cause to software, it’s redundant for us to delve further into the finer details of their use.
Trouble in paradise
The first sign that my relationship with switch
was on the ropes came to me slowly but surely: Over time, I discovered that software systems heavily reliant on switch
statements could prove difficult to change—rigid—and prone to bugs—fragile. Back then, I would not have known these problems by those terms.
Reason 1: switch
Statements Multiply
Let’s return to our payment gateways switch
example. We already have a switch
for paying an amount using a chosen payment gateway.
What if we had to configure each of these payment gateways before use?
We could end up with another, yet similar switch
for configuring the payment gateways:
switch (paymentGateway) { case "Paypal": // Configure Paypal. // ... code to configure the PayPal payment gateway. break; case "Stripe": // Configure Stripe. // ... code to configure the Stripe payment gateway. break; default: throw new UnknownPaymentGateway(paymentGateway); }
Now we have two separate switch
statements—one for configuration and one for payment—on essentially the same information: the payment gateway type.
OK, but that’s it, right?
Well, maybe, or maybe not. What if we had to handle transaction notifications from the payment gateways? Time for another switch
statement. And so the switch
‘s multiply and fester: We’d probably need five more switch
statements if we had five more payment gateway operations.
What starts as a single and seemingly innocuous switch
over time morphs into a mass of switch
statements. By and of itself, that is not an issue. However, we’ll face problems when we want to make code changes.
For example, consider needing to alter the spelling of “Paypal” to “PayPal”. We would need to make this change in the switch
statements selecting on payment gateway name. Without a suite of unit tests to rely on, we could easily forget to update a couple of these statements with the new spelling—and now we have bugs.
Making changes safely on multiple
switch
statements is time-consuming and error-prone without a reliable suite of unit tests.
To provide context, I have experienced situations where there were a dozen or more switch
statements, each for a different operation. In some of the worst examples, it was not uncommon to discover multiple switch
statements on the same reference variable in the same long (and convoluted) function!
Reason 2: switch
Statements Violate the Single Responsibility Principle
Here is another reason why switch
hurts our software: switch
statements violate the Single Responsibility Principle (SRP), which specifies that each software module (or class) should have only one responsibility. The originator of the SRP, Robert Martin, has expressed that this is an often misunderstood principle—it’s about each class only being responsible to one group of stakeholders that can demand changes to the code in that class.
To illustrate how switch statements break the SRP, let’s go back to the switch
for paying with a selected payment gateway. Again, here is the listing:
switch (paymentGateway) { case "Paypal": // Pay with Paypal. // ... code to pay using PayPal payment gateway. break; case "Stripe": // Pay with Stripe. // ... code to pay using Stripe payment gateway. break; default: throw new UnknownPaymentGateway(paymentGateway); }
Question: Who can force us to make changes to, say, the Stripe payment code? The answer is clear: It’s the developers at Stripe—when they modify their API or SDK, we must also change our client code. If we don’t, our Stripe payments will no longer work. The same goes for Paypal or another payment gateway.
On the other hand, who decides which payment gateways to use for payments in our software? The people who make those kinds of decisions at our company—Product Owners, managers and, ultimately, customers.
According to the SRP, because two different sets of people make decisions regarding the contents of the switch
statement, the switching itself and the code in the case
statements do not belong together. The Stripe client logic and the Paypal client logic should each live in separate classes—removed from the switch
statement.
Here is another way to look at it: We can think of picking a payment gateway as a high-level policy—the ability to pay with different payment gateways. Yet, the detail of how we implement each payment gateway client code is a low-level mechanism. In other words, that we can choose Stripe, Paypal, or another payment gateway is quite different from the actual workings of the Stripe or Paypal payment code. And it’s bad form to group high-level policy with low-level concrete mechanisms.
Reason 3: switch Statements Violate the Open-Closed Principle
Furthermore, switch
violates the Open-Closed Principle (OCP). This principle, part of the SOLID Principles, asserts that
“Software entities should be open for extension but closed for modification.“
As much as possible, we should be able to make changes in our programs by writing new code rather than changing existing code.
To illustrate, let’s come back to our standard payment gateway switch
:
switch (paymentGateway) { case "Paypal": // Pay with Paypal. // ... code to pay using PayPal payment gateway. break; case "Stripe": // Pay with Stripe. // ... code to pay using Stripe payment gateway. break; default: throw new UnknownPaymentGateway(paymentGateway); }
Depending on the name of the payment gateway, we will pay either with “Paypal” or “Stripe”.
What if we wanted to add another payment gateway called “DPS”? How would we achieve this with this switch
statement? Simple: We’ll add another case
statement for “DPS”:
switch (paymentGateway) { case "Paypal": // Pay with Paypal. // ... code to pay using PayPal payment gateway. break; case "Stripe": // Pay with Stripe. // ... code to pay using Stripe payment gateway. break; case "DPS": // Pay with DPS. // ... code to pay using DPS payment gateway. break; default: throw new UnknownPaymentGateway(paymentGateway); }
Since we have added another case for DPS, we are changing the existing source code. The OCP tut-tuts its disapproval—if possible, we ought to write new code.
Don’t get me wrong; this isn’t about blind adherence to some abstract-seeming programming principle. Including this single new case
for “DPS” isn’t a problem. Not yet. However, things will turn ugly quickly when we have many switch
es. If we have 23 separate switch
statements on the payment gateway name (or type), adding another payment gateway will be painful and error-prone.
Is it possible for us to write entirely new code to include the DPS payment gateway?
Sure, but we’ll need to look beyond the switch
statement: The Strategy Pattern
Conclusion
As versatile as the switch
statement appears, it represents a dangerous control flow mechanism. Again, let’s enumerate the problems:
- Over time,
switch
statements tend to multiply uncontrollably in our codebases, switch
statements mix high-level switching code with the low-level polymorphic implementations in the same module (or class), andswitch
requires us to change existing code rather than write new code.
When we have many switch
statements (1.) requiring us to change much of the existing code (3.), which lacks proper separation of concerns (2.), then it should come as no surprise that the switch
statement contributes so much harm to our programs.
Leave a Reply
Want to join the discussion?Feel free to contribute!