In software development, functions are the building blocks of any application. They are the primary way we structure our logic, encapsulate behavior, and divide complex tasks into manageable pieces.
However, not all functions are created equal. In Chapter 3 of Clean Code, the focus is on writing tasks that are small, focused, and do one thing well.
In this article, we'll dive into these principles and explore how to apply them in JavaScript.
📌1. Small Functions: Less Is More
One core tenet of Clean Code is that functions should be small.
But what does "small" mean in this context? The author advocates for functions that are typically only a few lines long.
The idea is that a small function is easier to understand, test, and maintain.
Let's look at an example in JavaScript:
// Bad Example: A large, unfocused function
function processOrder(order) {
applyDiscount(order);
calculateShipping(order);
calculateTax(order);
generateInvoice(order);
sendConfirmationEmail(order);
updateInventory(order);
}
This function, while seemingly straightforward, does too much. Each of these tasks could and should be broken down into its own function.
// Good Example: Small, focused functions
function processOrder(order) {
applyDiscount(order);
calculateShipping(order);
calculateTax(order);
generateInvoice(order);
sendConfirmationEmail(order);
updateInventory(order);
}
function applyDiscount(order) {
// Discount logic
}
function calculateShipping(order) {
// Shipping calculation logic
}
function calculateTax(order) {
// Tax calculation logic
}
function generateInvoice(order) {
// Invoice generation logic
}
function sendConfirmationEmail(order) {
// Email sending logic
}
function updateInventory(order) {
// Inventory update logic
}
In the revised example, each function does one thing, making the code easier to read and maintain.
The processOrder
function now serves as a high-level orchestrator, while smaller, more focused functions handle the details.
📌2. Do One Thing
A function should do one thing and do it well. If you find yourself writing a function that performs multiple tasks, it's a sign that you should refactor it into smaller functions.
Here's an example:
// Bad Example: A function doing multiple things
function formatAndSendEmail(email, subject, message) {
const formattedMessage = `<html><body>${message}</body></html>`;
sendEmail(email, subject, formattedMessage);
}
While this function seems concise, it's doing two things: formatting the message and sending the email. Instead, break it down:
// Good Example: Functions doing one thing
function formatMessage(message) {
return `<html><body>${message}</body></html>`;
}
function sendFormattedEmail(email, subject, message) {
const formattedMessage = formatMessage(message);
sendEmail(email, subject, formattedMessage);
}
Now, each function has a single responsibility, which makes the code easier to test and reuse.
The formatMessage
function can be tested independently of the email-sending logic.
📌3. Avoid Side Effects
Functions should minimize side effects, meaning they shouldn't alter the state of the system in unexpected ways. Functions with side effects can be harder to debug and reason about.
Here's an example:
// Bad Example: A function with a side effect
let globalCounter = 0;
function incrementCounter() {
globalCounter++;
}
The incrementCounter
function changes the global state, which can lead to bugs if not carefully managed.
A better approach is to return a new value and let the caller decide what to do with it:
// Good Example: Avoiding side effects
function incrementCounter(counter) {
return counter + 1;
}
globalCounter = incrementCounter(globalCounter);
By avoiding side effects, you make your functions more predictable and easier to work with.
📌4. Single Level of Abstraction
Functions should operate at a single level of abstraction. Mixing different levels of detail within the same function can make it harder to understand.
For example:
// Bad Example: Mixed levels of abstraction
function getUserData(userId) {
const user = database.fetchUserById(userId); // Low-level
return `${user.firstName} ${user.lastName} (${user.email})`; // High-level
}
Here, the function mixes the low-level database fetch with the high-level formatting of user data.
It’s better to separate these concerns:
// Good Example: Single level of abstraction
function getUser(userId) {
return database.fetchUserById(userId);
}
function formatUserData(user) {
return `${user.firstName} ${user.lastName} (${user.email})`;
}
const user = getUser(userId);
const formattedUserData = formatUserData(user);
Now, each function operates at a single level of abstraction, making the code clearer and easier to maintain.
Conclusion ⚡
Writing clean functions is a cornerstone of writing maintainable code.
By keeping functions small, ensuring they do one thing, avoiding side effects, and maintaining a single level of abstraction, you can create code that is easier to read, understand, and maintain.
As you continue to hone your skills in JavaScript, keep these principles from Clean Code in mind to write functions that truly embody the art of simplicity and clarity.
Top comments (0)