In the previous part we started on decomposing conditionals or breaking complex conditional logic into simpler readable functions. Today we'll look into the below techniques:
- Consolidating conditional expression: Gathering conditions that trigger the same action at a single place.
- Improving Nested Conditionals: Replacing nested conditionals with guard clauses.
- Reducing conditions through polymorphism (For OOP only): Put conditions in a superclass and override the methods in subclass to automatically perform actions without being dependent on multiple conditionals
Now let's look at the first technique.
Consolidating conditional expression
Whenever we spot multiple conditions resulting in the same action or returning the same values, we can gather them together and use them at the same place effectively reducing repetition and often giving contextual clarity.
Let's take a look at an example.
function isEmployeeEligibleForRaise(employee){
if (!hasEnoughSeniority(employee)) {
return false;
}
// There can be multiple operations in between the conditions.
// It doesn't matter if they are not effected by the operations
randomOperationsOne();
if (!gotRaiseRecently(employee)) {
return false;
}
randomOperationsTwo();
if (!highPerforming(employee)) {
return false;
}
return true;
}
In the above example the first three conditions all return the same output. Instead of having them at separate places or in separate conditions, we can consolidate all of them in a single conditional and return the same value like below:
function isEmployeeEligibleForRaise(employee) {
if (
!hasEnoughSeniority(employee) ||
!gotRaiseRecently(employee) ||
!highPerforming(employee)
) {
return false;
}
randomOperationsOne();
randomOperationsTwo();
return true
}
This way we can reduce the duplication in code and also keep responsible logics for a particular output at the same place. Which helps with both debugging and readability.
Improving Nested Conditionals
Nested conditionals can make code difficult to understand, especially when there are many nested levels with minimal separation. This complexity can obscure which conditions apply to which parts of the code.
Consider the following example:
function processOrder(order) {
if (order.isPaid) {
if (order.isInStock) {
if (order.isValid) {
return "Order processed successfully.";
}
return "Order is not valid.";
}
return "Order is not in stock.";
}
return "Order is not paid.";
}
By using guard clauses, we can simplify and flatten the structure:
function processOrder(order) {
if (!order.isPaid) return "Order is not paid.";
if (!order.isInStock) return "Order is not in stock.";
if (!order.isValid) return "Order is not valid.";
return "Order processed successfully.";
}
The above code does the same work but is much more readable while not losing the step by step separation of nested conditions.
Reducing conditions through polymorphism
Whenever conditionals are used for performing operations for conditions with very little variation between each conditions, instead of making multiple conditions we can simply create a class and subclasses with method overrides to replace complex conditions with simple polymorphism. Here's a good example of such case where we have a set of payment gateways like PayPal, Stripe and Authorize.net. We want to calculate the charges for each of these gateways. The charges are different for each of them. Like below:
function calculateCharges(gateway, amount) {
let charge;
if (gateway === 'PayPal') {
charge = amount * 0.03 + 0.30; // 3% fee + $0.30 fixed fee for PayPal
} else if (gateway === 'Stripe') {
charge = amount * 0.029 + 0.25; // 2.9% fee + $0.25 fixed fee for Stripe
} else if (gateway === 'AuthorizeNet') {
charge = amount * 0.025 + 0.20; // 2.5% fee + $0.20 fixed fee for Authorize.Net
} else {
throw new Error("Unsupported payment gateway");
}
return charge;
}
Instead of writing a condition for each of them, we can create a base class and override the charge method in each of the subclasses:
// Base class for payment gateway
class PaymentGateway {
constructor(amount) {
this.amount = amount;
}
// Default charge method to be overridden by subclasses
charge() {
throw new Error("charge() method must be implemented in subclass");
}
}
// PayPal subclass
class PayPal extends PaymentGateway {
charge() {
return this.amount * 0.03 + 0.30; // 3% fee + $0.30 fixed fee for PayPal
}
}
// Stripe subclass
class Stripe extends PaymentGateway {
charge() {
return this.amount * 0.029 + 0.25; // 2.9% fee + $0.25 fixed fee for Stripe
}
}
// Authorize.Net subclass
class AuthorizeNet extends PaymentGateway {
charge() {
return this.amount * 0.025 + 0.20; // 2.5% fee + $0.20 fixed fee for Authorize.Net
}
}
After declaring the main class and creating its subclasses we can simply use them in the function like below:
function calculateCharges(paymentGateway) {
return paymentGateway.charge();
}
This approach means there can now be any number of payment gateway and in no case would we have to create any large if else ladders and each case can be separately modified without worrying about other cases (paymentGateways). Changes in any subclasses implementation will also be applicable across the codebase so modifications won't have to be repeated.
These concludes the basic of improving or refactoring conditional statements in our code. One important thing to remember is each application needs to be applied in a case by case basis if its necessary. Different teams may have pre-agreed methods of handling such cases, in such case its better to follow convention instead of personal preferences. Code readability is much more about teams total understanding and agreement than personal preferences.
Side note: I am personally still a junior in terms of experience. So take all of my words with a bit of salt and consult team members before attempting to make major code refactoring. Many parts of a code may seemingly go against clean coding standards yet might be there for a good reason for that specific case.