Published on 2024-04-07 11:12 by Kiguri
Why does clean code matter?
Surely there are several times when you look at a piece of code and think, “What the heck is this?” or “Who wrote this?” or “I can’t understand this.” This is a common scenario in software development. The code is not just for the computer to understand; it is also for humans to read and understand. Clean code is a software development principle that emphasizes writing code that is easy to read, understand, and maintain. It is a key aspect of producing high-quality software that is efficient and bug-free.
Applying clean code principles to your codebase can have several benefits:
- Readability: Clean code is easy to read and understand. It is self-explanatory and does not require additional comments to explain what it does.
- Maintainability: Clean code is easy to maintain. It is modular and well-organized, making it easy to make changes and add new features.
- Efficiency: Clean code is efficient. It is optimized for performance and does not contain unnecessary or redundant code.
- Bug-free: Clean code is less prone to bugs. It is well-structured and follows best practices, reducing the chances of introducing bugs.
- Collaboration: Clean code is easy to collaborate on. It is easy for other developers to understand and work with, making it easier to work on a team.
- Professionalism: It shows that you care about your work and take pride in what you do and also shows respect for readers and future maintainers of your code.
How can I access whether my code is clean or not?
Determining the cleanliness of your codebase can be achieved through several measures. Key signs include thorough documentation, uniform formatting, and an orderly structured code repository.
Peer code evaluations serve as a vital checkpoint, spotlighting areas for improvement while ensuring adherence to established coding standards and norms.
Furthermore, the role of testing in maintaining code purity cannot be overstated. It verifies the operational integrity of the code and aids in the early detection of discrepancies.
Adopting a range of tools, methodologies, and standard practices can significantly aid in cultivating a code environment that prioritizes clarity, simplicity, and ease of upkeep.
However, it’s crucial to acknowledge the subjective nature of code cleanliness. Perspectives on what constitutes clean code can vary widely among individuals and projects. What may be perceived as clean and efficient in one context may not hold the same regard in another.
Despite this subjectivity, there exists a core set of principles that, if followed, can lead to the development of cleaner, more manageable code. Let’s explore these principles further.
Meaningful Names
This is the first thing you should consider when writing clean code. Names should reveal intent. If a name requires a comment, then the name does not reveal its intent. For example, consider the following code:
const d = 5; // elapsed time in days
The variable d
does not reveal its intent. A better name would be:
const elapsedTimeInDays = 5;
Avoid naming variables with affixes that are already present in the variable’s nature.
const nameString = "John Doe"; // Bad
const name = "John Doe"; // Good
const isFlag = true; // Bad
const flag = true; // Good
const accountList = []; // Bad
const accounts = []; // Good
Also, you should avoid using “Noise Words” in your variable names. Noise words are words that do not add any value to the variable name. For example:
const theAccounts = []; // Bad
const accounts = []; // Good
const userData = {}; // Bad
const userInfo = {}; // Bad
const user = {}; // user is enough because it is a variable, and it is obvious that it is data. // Good
There are several noise words that you should avoid, such as the
, data
, info
, object
, value
, etc.
Magic Numbers
A “magic number” in programming refers to a hard-coded value that appears in the source code without explanation of its meaning. These numbers are often used to control program behavior, specify sizes, limits, or special values, but they can make the code difficult to understand and maintain. The term “magic” implies a lack of clarity about how or why the number works in the context in which it is used.
Using magic numbers is generally considered poor practice because it decreases the readability and maintainability of code.
Bad
def validate_image_size(image):
if image.width * image.height > 1000000: # 1000000 is a magic number
return False
return True
Good
MAX_IMAGE_SIZE = 1000000
def validate_image_size(image):
if image.width * image.height > MAX_IMAGE_SIZE:
return False
return True
By defining a constant with a meaningful name, you can make the code more readable and easier to maintain. If the value of the constant needs to change, you only need to update it in one place.
Functions
Naming Functions
Naming function is as important as naming variables. Functions is always a verb or a verb phrase. For example, getUser
, validateUser
, saveUser
, etc.
Be consistent with your function names. If you are using getUser
for getting a user, then use saveUser
for saving a user, not saveUserData
or saveUserDetails
. Following "one word for each concept"
is a good way to name your functions. For example, the action of “getting” a user information can be described by some words like fetch
, retrieve
, get
, find
, search
, etc. So, you should choose one of these words and use it consistently throughout your codebase.
Bad
function getUser() {
// ...
}
function retrieveOrder() {
// ...
}
Good
function getUser() {
// ...
}
function getOrder() {
// ...
}
Below is the naming of CRUD actions that I often use in projects:
// Create
function createUser() {
// ...
}
// Read one
function getUser() {
// ...
}
// Read many
function listUsers(params) {
// I don't like to use getAllUsers because the function could come with a filter or pagination
// So, listUsers is more appropriate
// ...
}
// Update
function updateUser() {
// ...
}
// Delete
function deleteUser() {
// ...
}
Functions Should Do One Thing
In real-world projects, there are cases where functions span several hundred lines, or even thousands of lines. For those new to the project, this can truly be a nightmare as it becomes very difficult to read, understand, and maintain.
So while trying to create a function, you should ensure:
- Write small functions: Functions should be small, focusing on doing one thing and doing it well.
Bad
function fulfillOrder(customer, product) {
createOrderAndSendEmail(customer, product);
}
function createOrderAndSendEmail(customer, product) {
// ...
}
Good
function fulfillOrder(customer, product) {
const order = createOrder(customer, product);
sendEmail(order);
}
function createOrder(customer, product) {
// ...
}
function sendEmail(order) {
// ...
}
- Accepts no more than three arguments: The more arguments a function has, the more complex it becomes. If a function requires more than three arguments, consider using an object as a parameter.
Bad
function createOrder(
customerId,
customerName,
customerAddress,
productId,
quantity,
price,
discount,
tax,
shippingCost
) {
// ...
}
Good
function createOrder(customer, product) {
const { id, name address } = customer;
const { id, quantity, price, discount, tax, shippingCost } = product;
// ...
}
Don’t Repeat Yourself (DRY)
Reusability
You shouldn’t write the same code with the same logic or doing the same thing in multiple places. If you need to change the logic, you will have to change it in multiple places, which is error-prone and time-consuming.
To avoid this, you should extract the common code into a function and call that function wherever you need it so that you only need to change the logic in one place.
Bad
function calculateRectangleArea(length, width) {
return length * width;
}
function calculateCircleArea(radius) {
return Math.PI * radius * radius;
}
function calculateTriangleArea(base, height) {
return 0.5 * base * height;
}
let rectArea = calculateRectangleArea(5, 10);
let circleArea = calculateCircleArea(5);
let triangleArea = calculateTriangleArea(5, 10);
Good
function calculateArea(shape, ...args) {
switch (shape) {
case "rectangle":
return calculateRectangleArea(...args);
case "circle":
return calculateCircleArea(...args);
case "triangle":
return calculateTriangleArea(...args);
default:
throw new Error("Invalid shape");
}
}
let rectArea = calculateArea("rectangle", 5, 10);
let circleArea = calculateArea("circle", 5);
let triangleArea = calculateArea("triangle", 5, 10);
Single Source of Truth
The Single Source of Truth (SSOT) principle is a software development practice that promotes the idea of having a single, definitive source of data or information within a system. This principle is particularly relevant in the context of clean code, as it helps to reduce redundancy, inconsistencies, and errors in your codebase.
By following the SSOT principle, you can ensure that your code is more maintainable, scalable, and reliable. When there is a single source of truth for a particular piece of data or logic, you can be confident that any changes made to that source will be reflected accurately throughout your application.
For example, consider a scenario where you have multiple functions that need to access the same configuration settings. Instead of hardcoding these settings in each function, you can define them in a central configuration file and import them wherever they are needed. This way, if the configuration settings change, you only need to update them in one place, ensuring consistency across your codebase.
Bad
// get.js
let API_URL = "https://api.example.com";
let API_KEY;
function fetchData() {
return fetch(`${API_URL}?key=${API_KEY}`);
}
// create.js
let API_URL = "https://api.example.com";
let API_KEY;
function createData(data) {
return fetch(API_URL, {
method: "POST",
body: JSON.stringify(data),
headers: {
"Content-Type": "application/json",
},
});
}
Good
// config.js
export const API_URL = "https://api.example.com";
export const API_KEY = "";
// get.js
import { API_URL, API_KEY } from "./config";
function fetchData() {
return fetch(`${API_URL}?key=${API_KEY}`);
}
// create.js
import { API_URL } from "./config";
function createData(data) {
return fetch(API_URL, {
method: "POST",
body: JSON.stringify(data),
headers: {
"Content-Type": "application/json",
},
});
}
Comments
Avoid annotating your code with comments that merely describe what it’s doing. Ideally, the code should stand on its own, clear and comprehensible without additional explanation. Reserve comments for clarifying the reasons behind the code’s operations. If you find the need to use comments to explain what your code does, it might be time to rethink and refactor your code to enhance its readability.
Bad
// This function calculates the area of a rectangle
function calc(a, b) {
// a is the length of the rectangle
// b is the width of the rectangle
return a * b;
}
let area = calc(5, 10); // calculate the area of a rectangle with length 5 and width 10
Good
function calculateRectangleArea(length, width) {
return length * width;
}
let area = calculateRectangleArea(5, 10);
Commented-out code often clutters project repositories. It can lead to confusion for others debugging or reviewing your code, prompting questions about its purpose. Unnecessary code should be removed to maintain cleanliness. For code you might need in the future, rely on your version control system such as GIT to retrieve it. In cases where the code is needed under specific circumstances, such as development environments, consider implementing a feature flag or configuration file to toggle its availability.
Bad
// Don't need in production
// function debug() {
// // ...
// }
function main() {
// debug();
// ...
}
Good
function debug() {
// ...
}
function main() {
if (env === "development") {
debug();
}
}
Code Organization and Formatting
Code Organization
Organizing your code is essential for readability and maintainability. A well-organized codebase makes it easier to navigate, understand, and modify the code. On the other hand, a disorganized folder structure can lead to confusion, inefficiency, and difficulty in managing the project. It can result in wasted time searching for files, difficulty understanding the project’s architecture, and increased chances of introducing bugs.
Here are some best practices for organizing your code:
-
Group Related Code: Group related code together to make it easier to understand and maintain. For example, place all utility functions in a
utils
folder, all components in acomponents
folder, and all API calls in anapi
folder. -
Use Meaningful Names: Use meaningful names for files, folders, and variables to convey their purpose and functionality. Avoid generic names like
utils
orhelpers
and be specific about what each file or folder contains. -
Separate Concerns: Separate concerns by dividing your code into modules that handle specific tasks or features. This separation helps to keep your codebase organized and maintainable.
-
Avoid Nested Structures: Avoid deep nesting of folders and files, as this can make it difficult to navigate the codebase. Keep your folder structure flat and use subfolders only when necessary.
The following is an example of a well-organized folder structure for a React project, which is organized around features. Each feature has its own folder containing components, API calls, and routes. This structure makes it easy to locate and work on specific parts of the project.
├── public
├── src
│ ├── assets
│ │ ├── **/\*.css
│ │ ├── **/\*.png
│ ├── components
│ │ ├── Button
│ │ ├── Card
│ │ ├── Header
│ │ ├── index.ts
│ ├── constants
│ ├── features
│ │ ├── auth
│ │ │ ├── components
│ │ │ ├── api
│ │ │ ├── routes
│ │ │ ├── index.ts
│ │ ├── home
│ │ │ ├── components
│ │ │ ├── api
│ │ │ ├── routes
│ │ │ ├── index.ts
│ ├── lib
│ ├── routes
│ │ │ ├── index.ts
│ │ │ ├── PublicRoutes.tsx
│ │ │ ├── PrivateRoutes.tsx
│ ├── utilities
│ ├── App.tsx
│ ├── index.css
│ ├── main.tsx
Code Formatting
-
Whitespace and Indentation: Use consistent indentation and whitespace to improve the readability of your code. Whether you prefer two spaces, four spaces, or tabs, applying this choice consistently across your codebase will make it easier to read and understand the structure of your code.
-
Line Length and Wrapping: Keeping lines at a manageable length (e.g., 80-120 characters) can make your code easier to read, especially on devices or editors with limited horizontal space. When lines exceed your length limit, consider logical places to break them, such as after commas or operators, and indent continued lines to indicate they are part of the same statement.
-
Brace Style and Element Spacing: Consistency in brace style (e.g., K&R, Allman) and spacing around elements (e.g., operators, keywords) further enhances readability. Whether you choose to place opening braces on the same line as the statement or on a new line, apply your choice consistently throughout your codebase.
Code Linting and Formatting Tools
Leveraging tools can help automate and enforce consistency in code organization and formatting:
-
Linters and Formatters: Tools like ESLint for JavaScript, Pylint for Python can automatically flag style violations and sometimes fix them for you. Formatters like Prettier for JavaScript and Black for Python can reformat your code to meet specified style guidelines automatically.
-
Editor Configurations: Many code editors support configuration files (e.g., .editorconfig) that define and enforce basic formatting rules across different editors and IDEs, ensuring consistency even when multiple developers are working on the same project.
-
Pre-commit Hooks: Integrating formatting and linting tools with pre-commit hooks can ensure that all code commits meet your project’s formatting standards. Tools like Husky for node.js projects can automate this process.
Documentation
Effective documentation is as vital as the code itself. It serves as a guide for future maintainers, including your future self, and facilitates a deeper understanding of the code’s purpose and functionality. Different types of documentation serve various purposes:
-
Inline Comments: Use these for complex logic or when you’re using algorithms that might not be immediately obvious to other developers. Remember, good code explains ‘what’ and ‘how,’ but comments can clarify ‘why.’
-
API Docfumentation: Tools like JSDoc can generate detailed documentation for your functions, classes, and methods directly from your code comments. TypeScript also provides excellent support for generating documentation from type annotations. This is invaluable for both internal and external APIs, providing a clear contract for how to interact with your code.
-
README Files and Developer Guides: Your repository should include a README file that offers an overview of the project, setup instructions, and how to contribute. For larger projects, consider additional markdown files or a dedicated documentation site to cover architecture decisions, coding practices, and more detailed guides.
-
Keeping Documentation Updated: As your code evolves, so too should your documentation. Implement practices like requiring documentation updates as part of the code review process to ensure that changes in code are reflected in the documentation.
Conclusion
Wrapping up our clean code journey in a more cheerful note: Clean code isn’t just a bunch of rules that make our lives as developers harder; it’s the secret sauce that makes our code sparkle and shine! It’s what transforms a chaotic jumble of characters into a masterpiece of efficiency, readability, and sheer brilliance. By hugging the best practices and principles of clean code close, we’re not just coding—we’re crafting art that’s a joy to behold (and debug)!
So, if you’ve stuck with me this far, hats off to you! You’re on your way to becoming a maestro of the clean code symphony. Remember, like any good skill, mastering the art of clean code takes a bit of patience, a dash of practice, and a whole lot of passion. So keep on coding, keep on refining, and let’s make our codebases places of beauty and joy. Here’s to less head-scratching and more high-fiving. Happy coding, you brilliant coding wizards! 🎉👩💻🧙♂️✨
Written by Kiguri
← Back to blog