DEV Community

Lucas Pereira de Souza
Lucas Pereira de Souza

Posted on

Clean Code Principles

logotech

## Clean Code: The Art of Readability and Maintainability in the Backend

In the dynamic world of backend development, where complexity can grow exponentially, the quality of our code is a critical factor for success. Well-written code not only works correctly but is also easy to understand, maintain, and scale. This post explores three fundamental pillars of \"Clean Code\": readability, meaningful naming, and small functions, demonstrating how to apply them with examples in TypeScript/Node.js.

Introduction: The Cost of \"Dirty\" Code

Imagine inheriting an obscure codebase, filled with cryptic variable names and gigantic functions that resemble black boxes. Increasing productivity becomes an arduous task, and introducing new bugs is almost inevitable. This scenario is a direct result of neglecting good coding practices. Investing time in writing clean code is an investment in the project's future, reducing debugging time, facilitating collaboration among developers, and lowering overall maintenance costs.

Development: The Pillars of Clean Code

1. Readability: Writing for Humans

Code is read far more often than it is written. Therefore, the top priority should be clarity for the reader.

  • Consistent Formatting: Use a code formatter (like Prettier) to ensure a uniform style.
  • Adequate Spacing: Utilize whitespace to separate code blocks and make reading more fluid.
  • Strategic Comments: Comment only the \"why\" of a complex decision, not the \"what\" the code does (that should be clear from reading the code itself).

2. Meaningful Naming: Names That Speak for Themselves

Names of variables, functions, classes, and modules should reveal their intent. Avoid ambiguous abbreviations and generic names.

  • Be Descriptive: userCount is better than uc. calculateTotalPrice is better than calc.
  • Avoid Misleading Abstractions: Don't name a variable list if it contains a Map.
  • Verb-Noun Function Names: getUserById(userId: number) or saveOrder(order: Order).

3. Small Functions: Divide and Conquer

Functions should do one thing and do it well. Small functions are easier to understand, test, and reuse.

  • Single Responsibility Principle (SRP): Each function should have a single, clear responsibility.
  • Ideal Size: Ideally, a function should not exceed 15-20 lines. If a function is getting long, it's probably doing too much.
  • Refactoring: Break large functions into smaller, descriptively named units.

Code Examples (TypeScript/Node.js)

Let's refactor a hypothetical example to illustrate these practices.

Before (Code with Potential Issues):

// Function that processes an order and sends an email
function processOrder(data: any) {
  // Basic validation
  if (!data || !data.userId || !data.items || data.items.length === 0) {
    console.error(\"Invalid data\");
    return false;
  }

  let total = 0;
  for (const item of data.items) {
    total += item.price * item.quantity;
  }

  // Simulate DB persistence
  console.log(`Saving order for user ${data.userId} with total ${total}`);
  // Simulate email sending
  console.log(`Sending confirmation email to user ${data.userId}`);

  return true;
}
Enter fullscreen mode Exit fullscreen mode

After (Refactored Code with Best Practices):

/**
 * Represents an item in an order.
 */
interface OrderItem {
  productId: string;
  quantity: number;
  price: number;
}

/**
 * Represents the data structure of an order.
 */
interface Order {
  userId: number;
  items: OrderItem[];
  // other relevant properties like date, status, etc.
}

/**
 * Validates the basic structure of an order object.
 * @param order - The order object to validate.
 * @returns True if the order is valid, False otherwise.
 */
function isOrderValid(order: Order | null | undefined): boolean {
  if (!order) return false;
  if (!order.userId) return false;
  if (!order.items || order.items.length === 0) return false;
  // More specific validations for each item could be added here
  return true;
}

/**
 * Calculates the total amount of an order based on its items.
 * @param items - An array of order items.
 * @returns The calculated total amount.
 */
function calculateOrderTotal(items: OrderItem[]): number {
  return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}

/**
 * Simulates persisting an order to the database.
 * @param orderDetails - Details of the order to be saved.
 */
async function saveOrderToDatabase(orderDetails: { userId: number; totalAmount: number }): Promise<void> {
  console.log(`Persisting order for user ${orderDetails.userId} with total ${orderDetails.totalAmount}`);
  // Actual database persistence logic would go here.
  // Example: await db.orders.insert(orderDetails);
  await new Promise(resolve => setTimeout(resolve, 50)); // Simulates I/O latency
}

/**
 * Sends a confirmation email to the user.
 * @param userId - The user's ID.
 * @param totalAmount - The total order amount.
 */
async function sendOrderConfirmationEmail(userId: number, totalAmount: number): Promise<void> {
  console.log(`Sending confirmation email to user ${userId} with total ${totalAmount}`);
  // Actual email sending logic would go here.
  // Example: await emailService.sendConfirmation(userId, totalAmount);
  await new Promise(resolve => setTimeout(resolve, 50)); // Simulates I/O latency
}

/**
 * Processes an order, orchestrating validation, calculation, and persistence steps.
 * @param orderData - The raw order data.
 * @returns A Promise that resolves to true if processing is successful, false otherwise.
 */
async function processOrderClean(orderData: Order): Promise<boolean> {
  if (!isOrderValid(orderData)) {
    console.error(\"Order processing failed: Invalid data.\");
    return false;
  }

  const totalAmount = calculateOrderTotal(orderData.items);

  try {
    // Concurrency for I/O optimization
    await Promise.all([
      saveOrderToDatabase({ userId: orderData.userId, totalAmount }),
      sendOrderConfirmationEmail(orderData.userId, totalAmount),
    ]);
    console.log(`Order processed successfully for user ${orderData.userId}.`);
    return true;
  } catch (error) {
    console.error(`Error processing order for user ${orderData.userId}:`, error);
    return false;
  }
}

// Example usage:
const sampleOrder: Order = {
  userId: 123,
  items: [
    { productId: \"abc\", quantity: 2, price: 10.50 },
    { productId: \"def\", quantity: 1, price: 25.00 },
  ],
};

processOrderClean(sampleOrder)
  .then(success => console.log(\"Final processing result:\", success))
  .catch(err => console.error(\"Unexpected error:\", err));
Enter fullscreen mode Exit fullscreen mode

In the refactored code:

  • We use interface to define clear data structures.
  • The processOrder function has been broken down into isOrderValid, calculateOrderTotal, saveOrderToDatabase, sendOrderConfirmationEmail.
  • Names are descriptive (orderData, totalAmount, userId).
  • The main function processOrderClean now orchestrates calls to smaller, single-responsibility functions.
  • We use async/await and Promise.all to optimize I/O operations, demonstrating a common pattern in Node.js.
  • Comments explain the \"why\" (e.g., I/O optimization) and not the \"what."

Conclusion: A Continuous Investment

Adopting clean code practices, such as readability, meaningful naming, and creating small functions, is not a one-time task but an ongoing commitment. By prioritizing these principles, we build more robust, maintainable backend systems that foster a collaborative and productive development environment. Remember: clean code is code that respects the time and effort of its future maintainers.

Top comments (0)