Published on
-

10 Golden Rules for Writing Clean and Maintainable Functions with Clean Code 💻

7 min read - 1222 words
Authors
  • avatar
    Name
    Cédric RIBALTA
    Twitter
post image

Writing clean functions is a fundamental skill for any developer looking to improve the quality of their code. In this article, we'll explore the 10 best practices from Clean Code by Robert C. Martin (also known as Uncle Bob) 💻, to help you create efficient and maintainable functions. Whether you're a beginner or an experienced developer, these tips will help you write cleaner, more readable, and easier-to-maintain code 🛠️.

Why Are These Practices Important?

Before diving into the 10 best practices, it's crucial to understand why they matter. Well-written functions improve the readability and maintainability of your code. This allows other developers (or your future self) to quickly understand what a function does, test it, and modify it without having to rewrite large portions of code.

These practices aren’t just theoretical rules; they address practical needs that every developer faces daily: reducing complexity, minimizing bugs 🐛, and facilitating collaboration 🤝. Now that you understand their importance, let's dive into the 10 steps to follow.


1. The Shorter, the Better ✂️

The first rule of a clean function is its size. According to Uncle Bob, a function should be short. Ideally, it should not exceed 20 lines of code. Why? Because a short function is easier to read and understand. It also helps limit the number of responsibilities a function has (we’ll cover that later).

Let's take a simple example :

function processOrder(order) {
  if (order.isPaid) {
    updateStock(order.items)
    sendEmailConfirmation(order)
    generateInvoice(order)
  } else {
    notifyUser(order)
  }
}

Here is a refactored version, with shorter and more specific functions:

function processOrder(order) {
  if (order.isPaid) {
    handlePaidOrder(order)
  } else {
    notifyUser(order)
  }
}

function handlePaidOrder(order) {
  updateStock(order.items)
  sendEmailConfirmation(order)
  generateInvoice(order)
}

2. Easy on the Tab Key ⌨️

A good indicator of a function's complexity is its indentation level. The deeper the nesting, the more complex and harder it becomes to understand and maintain. Uncle Bob recommends not exceeding 2 levels of indentation. If you need more, it’s likely a sign that your function needs to be split up.

Here's an example of a function with excessive indentation:

function validateOrder(order) {
  if (order.isPaid) {
    if (order.isInStock) {
      processOrder(order)
    } else {
      console.log('Item not in stock')
    }
  } else {
    console.log('Order not paid')
  }
}

And here's a refactored version with reduced indentation:

function validateOrder(order) {
  if (!order.isPaid) {
    return handleUnpaidOrder()
  }
  if (!order.isInStock) {
    return handleOutOfStock()
  }
  processOrder(order)
}

The refactored version is easier to read and understand.


3. Single Responsibility 🎯

A function should do one thing and do it well ✅. If it does more than one thing, it's probably too complex. One way to check this is to read the function name: if you can’t describe it without using "and," then the function is doing too much.

Here's an example of a function that does too much:

function saveAndNotifyUser(user) {
  saveToDatabase(user)
  sendEmail(user.email)
}

And here's a refactored version with two separate functions:

function saveUser(user) {
  saveToDatabase(user)
}

function notifyUser(user) {
  sendEmail(user.email)
}

By splitting the function into two, you make each function more focused and easier to understand.


4. A Function is Like an Elevator 🛗

A function should either move you up or down a level of abstraction. This means you shouldn't mix different levels of abstraction within the same function. For example, if a function deals with both low-level details and high-level concepts, it’s time to break it apart 🚀.

A bad example mixing levels of abstraction:

function manageOrder(order) {
  if (order.isPaid) {
    console.log('Processing order')
    updateStock(order.items)
    sendEmailConfirmation(order)
  }
}

A refactored version with clear levels of abstraction:

function manageOrder(order) {
  if (order.isPaid) {
    handlePaidOrder(order)
  }
}

function handlePaidOrder(order) {
  updateStock(order.items)
  sendEmailConfirmation(order)
}

Each function now focuses on a specific level of abstraction, making the code easier to understand.


5. A Pretty Little Name 🏷️

The name of a function should immediately tell you what it does. If you need to add a comment to explain what your function does, the name isn’t clear enough. A simple rule: if the function name is clear, its code probably is too. A good function name instantly improves code readability.

Here's an example of a function with a vague name:

function process(data) {
  // Process the data
}

And here's a refactored version with a descriptive name:

function updateInventory(items) {
  // Process the data
}

A good name makes the function's purpose clear without needing additional comments.


6. The Ideal Number of Parameters 🔢

The ideal number of parameters for a function is 0. After that, one parameter is acceptable, but try to avoid functions with more than 3 parameters. Each additional parameter increases complexity and makes the function harder to understand. Grouping multiple parameters into an object can sometimes add clarity.

Here's an example of a function with too many parameters:

function createOrder(user, items, shippingAddress, billingAddress) {
  // Create the order
}

And here's a refactored version with a single parameter object:

function createOrder(order) {
  // Create the order
}

By grouping the parameters into an object, you reduce the number of parameters and make the function easier to use.


7. Flags 🚩

A flag is a boolean variable that alters a function’s behavior. Its use usually suggests that the function is doing too much. Instead of using a flag, it's better to split the function into two, each with a clear and unique responsibility.

Here's an example of a function that uses a flag:

function processOrder(order, isPaid) {
  if (isPaid) {
    updateStock(order.items)
  } else {
    notifyUser(order)
  }
}

Now, a better approach without a flag:

function processPaidOrder(order) {
  updateStock(order.items)
}

function processUnpaidOrder(order) {
  notifyUser(order)
}

By removing the flag, you create two separate functions, each with a clear purpose.


8. Side Effects ⚡

A function should not produce side effects. This means it shouldn’t modify global variables or the parameters passed to it. A side effect introduces a coupling between the function and its environment, which can lead to difficult-to-track bugs.

Here's an example of a function with side effects:

function addItemToCart(item) {
  cart.push(item)
}

And here's a refactored version without side effects:

function addItemToCart(cart, item) {
  return [...cart, item]
}

By returning a new cart instead of modifying the existing one, you avoid side effects and make the function more predictable.


9. Command Query Separation (CQS) 🔄

The principle of Command Query Separation states that a function should either modify the state of an object (command) or return information about that state (query), but never both at the same time. This helps clarify the function's role and makes the code more readable 👓.

Here's an example of a function that violates CQS:

function updateAndFetchUser(user) {
  user.updateProfile()
  return user
}

And here's a refactored version that separates the command and query:

function updateUser(user) {
  user.updateProfile()
}

function getUserInfo(user) {
  return user
}

By separating the command (update) and query (fetch), you make the function's intent clearer.


10. Favor Exceptions ❗

It’s better to use exceptions rather than error codes to handle errors. Exceptions allow you to separate error-handling code from business logic, making the latter more readable. Exceptions also facilitate error propagation to higher layers without cluttering business logic.

Here's an example of a function that uses error codes:

function saveUser(user) {
  if (!user.isValid()) {
    return -1
  }
  // Save the user
}

And here's a refactored version that uses exceptions:

function saveUser(user) {
  if (!user.isValid()) {
    throw new Error('Invalid user')
  }
  // Save the user
}

By using exceptions, you make error handling more explicit and separate it from the main logic.


Conclusion

By applying these 10 Clean Code practices, you'll be able to write cleaner, more readable, and more maintainable functions 🧹. Of course, there are exceptions, but it's essential to know these rules to apply them wisely. In summary, keep your functions short, clear, and free of side effects to improve your code quality.