Clean Your Javascript: Transform conditional statements — Part 1

DANIEL SSEJJEMBA
Stackademic
Published in
8 min readNov 20, 2023

--

Photo by Zan on Unsplash

In programming, especially within the dynamic and ever-evolving world of JavaScript, the journey from a novice to a seasoned professional is fraught with numerous challenges and learning curves. One critical insight I’ve gleaned from my extensive experience, both as an interviewee and as an interviewer, is the paramount importance of understanding the ‘why’ behind our coding choices. It’s this deep comprehension and the ability to articulate the reasoning behind each decision that truly sets exceptional talent apart.

Through countless interviews and interactions with fellow developers, I’ve observed a common thread: the best aren’t necessarily those who blindly follow trends or merely replicate what is deemed the ‘best practice’ by popular opinion. Rather, they are those who possess a profound understanding of their craft. These individuals can convincingly explain why specific decisions were made, recognizing that, as the old adage goes, there are many ways to ‘skin the cat.’ Their choices aren’t made on a whim; they are underpinned by solid arguments, a comprehensive understanding of potential risks, and well-thought-out mitigation strategies.

This revelation has inspired me to create the ‘Clean Your JavaScript’ series. This collection of writings is more than just a guide to programming; it’s a deep dive into the art of making subjective decisions in coding that elevate one from a competent programmer to a top-level professional.

In this series, we won’t just explore how to write code. We’ll delve into the nuances of writing clean, maintainable, and robust JavaScript. We’ll dissect various approaches, evaluate their merits, and understand their pitfalls. Each post will guide you in making informed decisions, backed by strong arguments and a clear understanding of alternative solutions and their associated risks.

The Hypothetical Case of the Car Discounts

Imagine a busy Wednesday afternoon, with my coding music playlist on (which by the way is very genre-fluid). And then I take note of a code push from my managers. I am aware that he is working on a feature to calculate the discounts on cars selected by a user, that will be displayed in other parts of the system.

I like reading people’s code because I often compare it with how I would have personally implemented this same code myself and pull out lessons and this is how I get a chance to improve my coding style and also how I manage to keep in sync with everything that’s going on around me just in case I am needed to give a hand in other teams where I wasn’t part of the initial implementation of things.

And when my manager makes this commit I notice it’s a straightforward function that calculates discounts based on car brands. It passes all the unit tests so functionally it’s all good. But as I look at the function something catches my attention. Let’s see if you guys can see what it is in the snippet below:

function calculateDiscount(carBrand) {
let discount;
switch(carBrand) {
case CAR_BRANDS.TOYOTA:
discount = 0.05; // 5% discount
break;
case CAR_BRANDS.FORD:
discount = 0.07; // 7% discount
break;
// Additional cases for other brands
default:
discount = 0;
}
return discount;
}

Well, I hope you too see it, that dreaded switch statement. I phoned my manager and mentioned that I wanted to go through a portion of his code changes with him since I think I have an improvement in mind and want to discuss it with him.

The phone call

“I was just going through your recent commit on the discount feature,” I started, once we were both on the line. “It’s functional and all, but I think there might be a more elegant way to handle the discount logic.”

“Oh? I’m all ears,” my manager responded, curiosity piqued.

“You know how we always strive for clean, maintainable code, right? Well, I noticed the switch statement for handling car discounts. It works fine now, but I'm thinking about future scalability and maintenance."

My manager was silent for a moment, then said, “Go on.”

"I wanted to point out how easily it can lead to errors as our application grows," I explained.

“Interesting. Can you give me an example?” my manager asked.

“Sure, let’s consider a scenario where we accidentally introduce a new case without a break statement." I quickly drafted a snippet to illustrate my point:

function calculateCarDiscount(carBrand) {
let discount;
switch(carBrand) {
case CAR_BRANDS.TOYOTA:
discount = 0.05; // 5% discount
break;
case CAR_BRANDS.FORD:
discount = 0.07; // 7% discount
// Accidentally forgot the break here
case CAR_BRANDS.BMW:
discount = 0.1; // 10% discount
break;
// Additional cases for other brands
default:
discount = 0;
}
return discount;
}

“In this modified version, if someone selects a Ford, they’ll unintentionally get a 10% discount instead of 7% because of the fall-through to the BMW case,” I explained.

“Ah, I see the problem,” my manager acknowledged. “That’s a subtle bug that could easily go unnoticed.”

“Exactly. And there’s more. What if we need to apply a complex discount logic for a particular brand? The switch statement can become bloated and unwieldy." I continued.

“We could refactor this with if-else statements in favor of the switch and this would look something like this.” I stated.

function calculateCarDiscount(carBrand) {
let discount;
if (carBrand === CAR_BRANDS.TOYOTA) {
discount = 0.05;
} else if (carBrand === CAR_BRANDS.FORD) {
discount = 0.07;
} else if (carBrand === CAR_BRANDS.BMW) {
discount = 0.1;
} // And so on for other brands
else {
discount = 0;
}
return discount;
}

“This approach does avoid the fall-through issue of switch statements," I continued. "But it introduces its own set of problems. For one, it's not very scalable. As we add more car brands, the function grows linearly, becoming more cumbersome."

My manager chimed in, “I see your point. A long chain of if-else statements can become quite unwieldy, and it's not much of an improvement in terms of maintainability."

“Exactly,” I agreed. “And there’s also the readability aspect. Imagine scrolling through dozens of if-else conditions. It gets hard to quickly pinpoint the logic for a specific car brand. Plus, like the switch, it violates the Open/Closed Principle."

“So, it seems if-else isn't the ideal solution either," my manager concluded. "What do you suggest we do instead?"

“Well, I suggest we go with the strategy pattern here” I murmured. “It neatly encapsulates each brand’s discount logic into separate entities, making the code more manageable and extendable. Adding or modifying a discount becomes a matter of updating the strategy object or function, without the need to alter the core logic of our calculation function.”

My manager pondered for a moment. “Alright, let’s go with the Strategy pattern then. It seems to address our concerns much better than switch or if-else. Can you start working on refactoring the code?"

“Absolutely,” I responded, eager to implement a cleaner, more efficient solution.

The Refactor

While the scenario described above might seem a bit exaggerated for illustrative purposes, it’s unlikely to encounter such a dynamic in the real world, where a manager appears less informed than a junior developer. Typically, experienced managers are well-versed in these coding practices and principles. However, I chose to portray the manager in this way for a specific reason in our hypothetical dialogue: to emphasize the importance of exploration and articulation in software development.

In reality, whether you’re discussing with a senior manager, a peer, or even a junior team member, the ability to explore various coding options and articulate the reasoning behind your choices is invaluable. It’s not just about knowing the best practices but also about understanding the context and being able to communicate your decisions effectively.

Now, let’s dive deeper into the Strategy pattern. We’ll explore how this pattern can elegantly replace the traditional switch cases or if-else chains, enhancing our code's readability, maintainability, and scalability.

Implementing the strategy pattern

The Strategy pattern involves defining a family of algorithms (in our case, discount calculations for different car brands), encapsulating each one, and making them interchangeable. This pattern lets the algorithm vary independently from clients that use it. Here’s how we can apply it to our scenario:

function getToyotaDiscount() {
return 0.05; // 5% discount for Toyota
}

function getFordDiscount() {
return 0.07; // 7% discount for Ford
}

function getBmwDiscount() {
return 0.1; // 10% discount for BMW
}

const discountStrategies = {
[CAR_BRANDS.TOYOTA]: getToyotaDiscount,
[CAR_BRANDS.FORD]: getFordDiscount,
[CAR_BRANDS.BMW]: getBmwDiscount,
// Add more strategies as needed
};

I like to call this strategy creation, notice how I am not going for the path of least resistance. I am not trying to use anonymous functions or simple number constants but actually created handlers for each car make which is just obvious by looking at this code.

This a mistake that I have witnessed so many developers fall for. They think less code is the same thing as good code. But the truth is that good code might mean more code, and sometimes it might even feel like an overkill. But in most cases the difference is really just by about an extra few lines of code and that can transform your code from something that answers just the current questions to something that will also work well with any anticipated changes that in the software world are inevitable.

Now after defining our strategies, we can create our core logic which I like to call the strategy selector. This is the part of the code that uses input to determine which strategy to invoke.

function calculateCarDiscount(car) {
const discountStrategyHandler = discountStrategies[car.brand];
if(typeof discountStrategyHandler === 'function'){
return discountStrategyHandler(car);
}else {
// Implement a default handler
return defaultBrandDiscountHandler(car);
}
}

Here is one way we can implement our central logic. Notice how I now accept the entire car as input and not just the brand. Also, notice how I pass on the car object even though my handlers from before didn’t need it. I am making these subjective decisions because I can anticipate a usage where the calculation of the discount doesn’t just rely on the brand. But maybe BMW uses different strategies for “SUVs” and “Saloons”.

So I have written my function this way thinking about possible future use cases and also making sure the code is not fragile. Don’t get me wrong, this code is not perfect. For example, due to the prototypical nature of JavaScript, I can break this code if I use a car with a brand toString. And based on how you are using this function you can figure out how to safeguard your central logic further.

But generally looking at the code that the manager committed and our current iteration, we realize that with the current iteration, we can actually easily read out the algorithm and it would make sense even for a person who is not very familiar with JavaScript.

Conclusion

The crux of our discussion hinges on informed decision-making. Whether you choose to stick with switch statements or opt for alternatives like the Strategy pattern, the key is to have clear, reasoned justifications for your choice. It's about being able to articulate your reasoning in a constructive manner, rather than resorting to defensive or dismissive responses when your code is under scrutiny.

As developers, we must move beyond unhelpful excuses like “Even Google doesn’t do that,” and instead strive to cultivate a mindset of continuous learning and open, meaningful dialogues about our coding practices. The goal is to evolve not just as individual coders but as contributors to a larger community of software development, where sharing insights and discussing alternatives enrich everyone’s understanding and skill.

I look forward to continuing this journey with you in the next installment of ‘Clean Your JavaScript.’ Until then, happy coding, and remember: the strength of your code lies as much in its logic as in the reasoning that underpins it.

Stackademic

Thank you for reading until the end. Before you go:

  • Please consider clapping and following the writer! 👏
  • Follow us on Twitter(X), LinkedIn, and YouTube.
  • Visit Stackademic.com to find out more about how we are democratizing free programming education around the world.

--

--