switch

3 Reasons Why The switch Statement Is Killing Our Software

switch

Photo by Jaye Haych on Unsplash

 

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 switchs 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 switches. 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:

  1. Over time, switch statements tend to multiply uncontrollably in our codebases,
  2. switch statements mix high-level switching code with the low-level polymorphic implementations in the same module (or class), and
  3. switch 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. 

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply