Introduction:

Ensuring your code is clear and maintainable is very important for the success of any software endeavour. Nevertheless, as time progresses, the complexity of your code may escalate, leading to confusion and making it increasingly challenging to comprehend, alter, and sustain. This is where the practice of Refactoring becomes pivotal.

What is Refactoring?

Refactoring is the process of modifying the structure of your code without affecting its functionality. It aims to improve the code's readability, maintainability, performance, and overall quality. Think of it as rearranging your furniture: you're not changing it but optimizing its placement for better flow and functionality.

Benefits of Refactoring:

There are numerous benefits to refactoring your code, including:

  • Elevated code quality: Restructured code becomes more accessible for comprehension and modifications, reducing the likelihood of errors and facilitating smoother maintenance.
  • Improved maintainability: A well-organized code structure simplifies comprehension for fellow developers, encouraging their contributions to the codebase.
  • Enhanced productivity: Refactoring diminishes the time spent on debugging and issue troubleshooting, enabling developers to concentrate on developing new features.
  • Optimal performance: Streamlined code structure and the removal of redundancies have the potential to enhance the application's overall performance.
  • Augmented team collaboration: Enforcing consistent code quality standards fosters improved collaboration and knowledge exchange among the development team.

Common Refactoring Techniques:

Here are some common refactoring techniques:

  1. Extract method: Repeating blocks of code can be extracted into separate methods for improved reuse and reduced duplication.

Example:

function calculateTotalOrderValue(order: Order) {
  const subtotal = order.items.reduce((acc, item) => acc + item.price *      item.quantity, 0);
  const discount = applyDiscountRules(subtotal, order.customerType);
  const tax = calculateTax(subtotal, order.shippingAddress.state);
  const shipping = calculateShippingCost(order.items.length, order.shippingAddress.distance);
  const total = subtotal - discount + tax + shipping;
  return total;
}

The code before Refactoring

function calculateTotalOrderValue(order: Order) {
  const subtotal = calculateSubtotal(order);
  const discount = applyDiscountRules(subtotal, order.customerType);
  const tax = calculateTax(subtotal, order.shippingAddress.state);
  const shipping = calculateShippingCost(order.items.length, order.shippingAddress.distance);
  const total = subtotal - discount + tax + shipping;
  return total;
}

function calculateSubtotal(order: Order) {
  return order.items.reduce((acc, item) => acc + item.price * item.quantity, 0);
}

The code after Refactoring

In some cases, like this one, our code can get cluttered with repeated calculations. Imagine having duplicate grocery lists scattered throughout your kitchen! Refactoring with the Extract Method technique helps clean things up. We identify these repeated blocks and create a new, self-contained function for them, giving them a descriptive name like calculateSubtotal. Instead of repeating the same steps multiple times, we simply call this new function from where it's needed. This cleans up the original code and makes it easier to understand, maintain, and even reuse that calculation logic elsewhere. It's like having one organized recipe instead of multiple scribbled notes!

  1. Extract variable: Complicated expressions or values used multiple times can be stored in variables for better readability and reduced duplication.

Example:

function calculateShippingCost(itemCount: number, distance: number) {
  const baseCost = 10;
  const distanceMultiplier = 0.3;
  
  if (itemCount > 10 || distance > 500) {
    baseCost = 20;
    distanceMultiplier = 0.5;
  }

  return baseCost + distanceMultiplier * distance;
}

Before Refactoring

function calculateShippingCost(itemCount: number, distance: number) {
  const baseCost = 10;
  const distanceMultiplier = 0.3;
  
  if (itemCount > 10 || distance > 500) {
    baseCost = 20;
    distanceMultiplier = 0.5;
  }

  return baseCost + distanceMultiplier * distance;
}

After Refactoring

Sometimes, our code gets tangled with messy calculations, like trying to decipher a grocery list scribbled on different scraps of paper. Refactoring with the Extract Variable technique helps us clean things up. We identify these complex expressions with repeated values and give them their own named homes, like calling them baseCost or distanceMultiplier. Instead of repeating the calculation each time, we simply refer to our named variable friends. This makes the code tidier and easier to understand, maintain, and even test! It's like having everything neatly labelled and organized; no more hunting for buried values in cryptic expressions.

  1. Rename variable/function: Descriptive names improve understanding and clarify the purpose of the code.

Example:

function calcTotal(items: number[]): number {
  let sum = 0;
  for (let i = 0; i < items.length; i++) {
    sum += items[i];
  }
  return sum;
}

Before Refactoring

function calculateOrderTotal(orderItems: number[]): number {
  let orderTotal = 0;
  for (let itemIndex = 0; itemIndex < orderItems.length; itemIndex++) {
    orderTotal += orderItems[itemIndex];
  }
  return orderTotal;
}

After Refactoring

Refactoring with descriptive names is like turning on a flashlight in a dark room. Suddenly, you can see what everything is and where it belongs. It's like giving your code a GPS for better navigation and understanding. By replacing vague names like x or doStuff with meaningful ones like customerAddress or processPayment , you create a shared language that makes your code easier to read, maintain, and collaborate on. It's like giving your code a clear voice that everyone can understand, preventing misunderstandings and empowering teamwork.

  1. Move method/function: Reorganizing code by moving methods/functions to more appropriate locations enhances code structure and organization.

Example:

class ProductReview {
  constructor(private product: Product) {}

  displayReview() {
    const reviewRating = this.calculateReviewRating();
    console.log(`Review for ${this.product.name}: ${reviewRating} stars`);
  }

  calculateReviewRating(): number {
    // Logic to calculate review rating based on product data
  }
}

Before Refactoring

class Product {
  calculateReviewRating(): number {
    // Logic to calculate review rating based on product data
  }
}

class ProductReview {
  constructor(private product: Product) {}

  displayReview() {
    const reviewRating = this.product.calculateReviewRating();
    console.log(`Review for ${this.product.name}: ${reviewRating} stars`);
  }
}

After Refactoring

  1. Encapsulate Field: Creating getter and setter methods for fields enhances data security and access control.

Example:

class User {
  public name: string;
  public age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

Before Refactoring

class User {
  private _name: string;
  private _age: number;

  constructor(name: string, age: number) {
    this._name = name;
    this._age = age;
  }

  get name(): string {
    return this._name;
  }

  set name(newName: string) {
    if (newName.length > 0) {
      this._name = newName;
    } else {
      throw new Error("Name cannot be empty");
    }
  }

  get age(): number {
    return this._age;
  }

  set age(newAge: number) {
    if (newAge >= 18) {
      this._age = newAge;
    } else {
      throw new Error("Age must be 18 or older");
    }
  }
}

After Refactoring

Think of code as a fortress. Leaving name and age exposed is inviting trouble. Refactoring with the Encapsulate Field technique builds defensive walls. We make these fields private, then create controlled gateways called getters and setters. These guards check incoming data, ensuring it's valid and secure before letting it through. The result? A code fortress protected from unwanted access and data mayhem!

  1. Replace magic numbers with constants: Hardcoded values can be replaced with constants for improved readability and easier modification.

Example:

Tfunction calculateShippingCost(distance: number) {
  if (distance > 500) {
    return 20 + 0.5 * distance;
  } else {
    return 10 + 0.3 * distance;
  }
}

Before Refactoring

const BASE_SHIPPING_COST = 10;
const DISTANCE_MULTIPLIER = 0.3;
const LONG_DISTANCE_THRESHOLD = 500;
const LONG_DISTANCE_SURCHARGE = 10;
const LONG_DISTANCE_MULTIPLIER = 0.5;

function calculateShippingCost(distance: number) {
  if (distance > LONG_DISTANCE_THRESHOLD) {
    return BASE_SHIPPING_COST + LONG_DISTANCE_SURCHARGE + LONG_DISTANCE_MULTIPLIER * distance;
  } else {
    return BASE_SHIPPING_COST + DISTANCE_MULTIPLIER * distance;
  }
}

After Refactoring

  1. Code with Many Awaits: Utilize Promise.all to wait for multiple promises simultaneously, improving performance.

Example:

async function fetchDataSequentially() {
  const userData = await fetchUserData();
  const productData = await fetchProductData();
  const orderData = await fetchOrderData();

  // Process combined data
  processData(userData, productData, orderData);
}

Before Refactoring

async function fetchDataConcurrently() {
  const [userData, productData, orderData] = await Promise.all([
    fetchUserData(),
    fetchProductData(),
    fetchOrderData()
  ]);

  // Process combined data
  processData(userData, productData, orderData);
}

After Refactoring

Promise.all streamlines handling multiple promises, executing them concurrently instead of waiting for each one sequentially. This often improves performance, especially for independent tasks like fetching data from different APIs. It's like sending out multiple couriers simultaneously instead of waiting for each one to return before dispatching the next, potentially saving time and boosting efficiency.

  1. Multiple Database Calls: Consider batching multiple calls together or using libraries like axios-batch for efficient data retrieval. Batching multiple database calls reduces network overhead and improves performance by sending requests in a single batch instead of individually. Libraries like axios-batch simplify this process, potentially reducing round-trip times and speeding up data retrieval. It's like sending a single, well-packed shipment instead of multiple couriers, minimizing travel time and streamlining information delivery.

Example:

import axios from 'axios';
import axiosBatch from 'axios-batch';

const batch = [
  axios.get('/api/users'),
  axios.get('/api/products'),
  axios.get('/api/orders')
];

axiosBatch(batch)
  .then(results => {
    const [userData, productData, orderData] = results.map(response => response.data);
    // Process combined data
    processData(userData, productData, orderData);
  })
  .catch(error => {
    // Handle errors
  });

Code Example of multiple database calls

  1. Duplicate Code: SonarQube can identify and suggest solutions for eliminating duplicate code and improving code structure and maintainability.

Conclusion

Refactoring is a valuable tool for enhancing the quality and maintainability of your codebase. Incorporating the techniques and extensions highlighted in this blog can improve your code and advance your development process. It's important to acknowledge that Refactoring is an ongoing effort, so don't hesitate to regularly review your code and make necessary enhancements. Embrace the opportunity to continuously refine your code and elevate your development practices.