Best Clean Code Patterns to Use Everyday in Web Apps
Enhancing Readability and Maintainability for Seamless Development
Clean code is a philosophy for thinking about software in a manner where there is a continuous emphasis on writing code that is efficient, readable and simple. Many times enginers build us delusions for what is considered great code but often turns out to be overly complex solutions that require much more maintenance.
In this short post, we explore some of the basic clean code principles that can understand simply and apply immediately ti any new or existing codebase, whether big or small:
Principles
Naming Conventions
DRY Principle (Don't Repeat Yourself)
Single Responsibility Principle
Use of Pure Functions
Avoid Magic Numbers and Strings
Modularization
Error Handling
Commenting and Documentation
1. Naming Conventions
Consistent naming conventions make your code easier to read and understand
Always use descriptive names for variables, functions, and classes
Always be more more explicit in your naming over implicit, allowing the others that touch your code in the future to make less assumptions
Recommendations
Use camelCase for variables and functions
Use PascalCase for classes and components
Use and SCREAMING_SNAKE_CASE for constants
Example
Bad:
const n = 5;
const idx = 0;
def cpytxt(src, dst):
with open(src, 'r') as f:
data = f.read()
with open(dst, 'w') as f:
f.write(data)
def calc(x, y):
return x * y
class prsn:
def __init__(self, nm, ag):
self.nm = nm
self.ag = ag
def gtnm(self):
return self.nm
def gtg(self):
return self.ag
Good:
const numberOfItems = 5;
def copy_text(source_file, destination_file):
with open(source_file, 'r') as file:
data = file.read()
with open(destination_file, 'w') as file:
file.write(data)
def calculate_product(x, y):
return x * y
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def get_name(self):
return self.name
def get_age(self):
return self.age
const MAX_USERS = 100;
class UserProfile {
constructor(name, email) {
this.name = name;
this.email = email;
}
}
function getUserName(user) {
return user.name;
}
2. DRY Principle (Don't Repeat Yourself)
Avoid duplicating code
Make every attempt to create encapsulated, repetable logic blocks that can be used in multiple places across your app - this can be done with functions and classes
Example
Bad:
const createAdminUser = (name, email) => {
return {
name,
email,
role: 'admin'
};
};
const createRegularUser = (name, email) => {
return {
name,
email,
role: 'regular'
};
};
Good:
const createUser = (name, email, role = 'regular') => {
return {
name,
email,
role
};
};
const admin = createUser('Alice', 'alice@example.com', 'admin');
const regular = createUser('Bob', 'bob@example.com');
3. Single Responsibility Principle
Attempt to continuously and rigorously simplify your code by splitting up modules and blocks into the smallest of possible blocks - known as atomicity
If you have a function or class that does multiple things, you should create methods that have a singular objective
SRP makes your code easier to test, maintain, and reuse
Example
Bad:
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
save() {
// Save user to database
}
sendWelcomeEmail() {
// Send welcome email
}
}
Good:
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
}
class UserRepository {
save(user) {
// Save user to database
}
}
class EmailService {
sendWelcomeEmail(user) {
// Send welcome email
}
}
4. Use of Pure Functions
Pure functions is a mathemtical concept that can be applied to programming
Pure functions are functions that always return the same output for the same input - this implies that they are functions that have no side effects.
Pure functions are easier to test and reason about
Example 1
Bad:
let total = 0;
function addToTotal(amount) {
total += amount;
}
Good:
function calculateTotal(total, amount) {
return total + amount;
}
const total = calculateTotal(0, 5); // 5
Example 2
Normal, non-pure function approach:
This is a non-pure function because the result depends on an uncontrolled variable that it outside of the implementation. This is known as a side-effect.
from datetime import datetime
import requests
def fetch_weather(city):
response = requests.get(f'http://api.weatherapi.com/v1/current.json?key=API_KEY&q={city}')
return response.json()
def log_user_login(user_id):
with open('logins.txt', 'a') as file:
file.write(f'User {user_id} logged in at {datetime.now()}\n')
weather = fetch_weather('London')
# The result depends on the current weather in London and can change over time
# This function writes to a file, which is a side effect
log_user_login(123)
Pure function approach:
def filter_active_users(users):
return [user for user in users if user['is_active']]
users = [
{'id': 1, 'name': 'Alice', 'is_active': True},
{'id': 2, 'name': 'Bob', 'is_active': False},
{'id': 3, 'name': 'Charlie', 'is_active': True},
]
# Always returns the same list of active users given the same input list
active_users = filter_active_users(users)
5. Avoid Magic Numbers and Strings
This returns also to the first principle of naming conventions:
Often in development, you will have numbers specific to a specific environment, a port number, or anything similar - it’s always recommend that you avoid ‘lose’ or ‘magic’ numbers and to convert them to constant variables
This is primarily for your future self and other developers, in order to understand what the number actually means - sometimes it will be clear but other times you may forget the meaning behind it
Example
Bad:
if (status === 200) {
console.log('Success');
}
Good:
const HTTP_STATUS_OK = 200;
if (status === HTTP_STATUS_OK) {
console.log('Success');
}
6. Modularization
Modularization also comes back to the earlier principle of atomicity, which has the primary purpose of breaking things into their smallest pure function objectives. Once you go down the rabbit hole, we begin to package these atoms into molecules and more complex structures - in programming, these are referred to as modules, which we should design also in a way that can be reused in our app.
Example
Bad:
// all code in one file
const users = [];
function addUser(name, email) {
users.push({ name, email });
}
function getUser(email) {
return users.find(user => user.email === email);
}
Good:
// userService.js
const users = [];
function addUser(name, email) {
users.push({ name, email });
}
function getUser(email) {
return users.find(user => user.email === email);
}
module.exports = { addUser, getUser };
// main.js
const userService = require('./userService');
userService.addUser('Alice', 'alice@example.com');
const user = userService.getUser('alice@example.com');
console.log(user);
7. Error Handling
Finally, error handling is an essential piece of any modern application:
Proper error handling will prevent your server from crashing, reducing the downtime of your application for when users access it
This contributes to positive user experience as well
Error handling can refer to all of the following:
Establishing multiple error levels of alerts
Establishing explicit and clear error messages
Establishing catch blocks to prevent from errors taking down your app
Example
Bad:
function getUser(id) {
return database.findUserById(id);
}
Good:
function getUser(id) {
try {
return database.findUserById(id);
} catch (error) {
console.error('Error fetching user:', error);
throw new Error('Could not fetch user');
}
}