Clean Code Principles
Introduction
Clean code is easy to read, understand, and maintain. This guide covers SOLID principles, design patterns, refactoring techniques, code smells, naming conventions, and best practices for writing maintainable software.
1. SOLID Principles
// S - Single Responsibility Principle
// Each class should have one reason to change
// β Bad: User class doing too much
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
save() {
// Database logic
db.insert('users', this);
}
sendEmail(message) {
// Email logic
emailService.send(this.email, message);
}
generateReport() {
// Reporting logic
return `User Report: ${this.name}`;
}
}
// β
Good: Separate responsibilities
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
}
class UserRepository {
save(user) {
db.insert('users', user);
}
findById(id) {
return db.findOne('users', { id });
}
}
class UserNotifier {
sendEmail(user, message) {
emailService.send(user.email, message);
}
}
class UserReportGenerator {
generate(user) {
return `User Report: ${user.name}`;
}
}
// O - Open/Closed Principle
// Open for extension, closed for modification
// β Bad: Modifying class for new payment methods
class PaymentProcessor {
process(order, method) {
if (method === 'credit_card') {
// Credit card logic
} else if (method === 'paypal') {
// PayPal logic
} else if (method === 'crypto') {
// Crypto logic
}
}
}
// β
Good: Extend with new implementations
class PaymentProcessor {
constructor(paymentMethod) {
this.paymentMethod = paymentMethod;
}
process(order) {
return this.paymentMethod.charge(order);
}
}
class CreditCardPayment {
charge(order) {
// Credit card logic
return { success: true, transactionId: 'cc_123' };
}
}
class PayPalPayment {
charge(order) {
// PayPal logic
return { success: true, transactionId: 'pp_456' };
}
}
class CryptoPayment {
charge(order) {
// Crypto logic
return { success: true, transactionId: 'crypto_789' };
}
}
// Usage
const processor = new PaymentProcessor(new CreditCardPayment());
processor.process(order);
// L - Liskov Substitution Principle
// Subtypes must be substitutable for their base types
// β Bad: Square violates Rectangle behavior
class Rectangle {
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Rectangle {
setWidth(width) {
this.width = width;
this.height = width; // Violates expectation
}
setHeight(height) {
this.width = height;
this.height = height;
}
}
// β
Good: Use composition or separate hierarchies
class Shape {
getArea() {
throw new Error('Must implement getArea');
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Shape {
constructor(side) {
super();
this.side = side;
}
getArea() {
return this.side * this.side;
}
}
// I - Interface Segregation Principle
// Don't force clients to depend on interfaces they don't use
// β Bad: Fat interface
class Worker {
work() {}
eat() {}
sleep() {}
}
class Robot extends Worker {
work() {
// Robots work
}
eat() {
// Robots don't eat!
throw new Error('Robots cannot eat');
}
sleep() {
// Robots don't sleep!
throw new Error('Robots cannot sleep');
}
}
// β
Good: Segregated interfaces
class Workable {
work() {}
}
class Eatable {
eat() {}
}
class Sleepable {
sleep() {}
}
class Human {
constructor() {
this.workable = new Workable();
this.eatable = new Eatable();
this.sleepable = new Sleepable();
}
}
class Robot {
constructor() {
this.workable = new Workable();
}
}
// D - Dependency Inversion Principle
// Depend on abstractions, not concretions
// β Bad: High-level module depends on low-level module
class MySQLDatabase {
save(data) {
// MySQL-specific code
}
}
class UserService {
constructor() {
this.db = new MySQLDatabase(); // Tight coupling
}
saveUser(user) {
this.db.save(user);
}
}
// β
Good: Depend on abstraction
class Database {
save(data) {
throw new Error('Must implement save');
}
}
class MySQLDatabase extends Database {
save(data) {
// MySQL implementation
}
}
class MongoDatabase extends Database {
save(data) {
// MongoDB implementation
}
}
class UserService {
constructor(database) {
this.db = database; // Dependency injection
}
saveUser(user) {
this.db.save(user);
}
}
// Usage
const service = new UserService(new MySQLDatabase());
// Or easily switch to
const mongoService = new UserService(new MongoDatabase());
2. Naming Conventions
// β Bad naming
function calc(a, b) {
return a + b;
}
let d = new Date();
let x = getUserData();
// β
Good naming
function calculateTotalPrice(basePrice, taxRate) {
return basePrice * (1 + taxRate);
}
const currentDate = new Date();
const userProfile = getUserData();
// Naming guidelines
// 1. Use descriptive names
// β Bad
let tmp = user.name.split(' ');
// β
Good
let nameParts = user.name.split(' ');
// 2. Avoid abbreviations
// β Bad
function usrMgr() {}
const maxCnt = 100;
// β
Good
function userManager() {}
const maximumCount = 100;
// 3. Use verbs for functions
// β Bad
function data() {}
function user() {}
// β
Good
function fetchData() {}
function createUser() {}
function deleteUser() {}
function validateEmail() {}
// 4. Use nouns for variables
// β Bad
let calculate = 100;
let process = true;
// β
Good
let totalPrice = 100;
let isProcessed = true;
// 5. Boolean names
// β Bad
let valid = true;
let enabled = false;
// β
Good
let isValid = true;
let isEnabled = false;
let hasPermission = true;
let canEdit = false;
// 6. Constants in UPPER_CASE
const MAX_RETRY_ATTEMPTS = 3;
const API_BASE_URL = 'https://api.example.com';
const DEFAULT_TIMEOUT_MS = 5000;
// 7. Classes use PascalCase
class UserAccount {}
class PaymentProcessor {}
class EmailService {}
// 8. Private properties with #
class BankAccount {
#balance = 0;
deposit(amount) {
this.#balance += amount;
}
getBalance() {
return this.#balance;
}
}
3. Code Smells & Refactoring
// Code Smell: Long Method
// β Bad: Method doing too much
function processOrder(order) {
// Validate order
if (!order.items || order.items.length === 0) {
throw new Error('No items');
}
// Calculate total
let total = 0;
for (const item of order.items) {
total += item.price * item.quantity;
}
// Apply discount
if (order.discountCode) {
const discount = getDiscount(order.discountCode);
total = total * (1 - discount);
}
// Calculate tax
const tax = total * 0.1;
total += tax;
// Save to database
db.insert('orders', { ...order, total });
// Send email
emailService.send(order.customerEmail, 'Order Confirmed', `Total: $${total}`);
}
// β
Good: Extract methods
function processOrder(order) {
validateOrder(order);
const total = calculateOrderTotal(order);
saveOrder(order, total);
notifyCustomer(order, total);
}
function validateOrder(order) {
if (!order.items || order.items.length === 0) {
throw new Error('No items in order');
}
}
function calculateOrderTotal(order) {
let subtotal = calculateSubtotal(order.items);
subtotal = applyDiscount(subtotal, order.discountCode);
return addTax(subtotal);
}
function calculateSubtotal(items) {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
function applyDiscount(amount, discountCode) {
if (!discountCode) return amount;
const discount = getDiscount(discountCode);
return amount * (1 - discount);
}
function addTax(amount) {
const TAX_RATE = 0.1;
return amount * (1 + TAX_RATE);
}
// Code Smell: Duplicate Code
// β Bad: Repeated logic
function getUserOrders(userId) {
const user = db.findOne('users', { id: userId });
if (!user) throw new Error('User not found');
return db.find('orders', { userId });
}
function getUserProfile(userId) {
const user = db.findOne('users', { id: userId });
if (!user) throw new Error('User not found');
return user;
}
// β
Good: Extract common logic
function findUserById(userId) {
const user = db.findOne('users', { id: userId });
if (!user) throw new Error('User not found');
return user;
}
function getUserOrders(userId) {
findUserById(userId); // Validates user exists
return db.find('orders', { userId });
}
function getUserProfile(userId) {
return findUserById(userId);
}
// Code Smell: Long Parameter List
// β Bad: Too many parameters
function createUser(name, email, password, age, city, country, phone, address) {
// ...
}
// β
Good: Use object parameter
function createUser({ name, email, password, age, city, country, phone, address }) {
// ...
}
// Usage
createUser({
name: 'John',
email: 'john@example.com',
password: 'secure123',
age: 30
});
// Code Smell: Large Class
// β Bad: God class doing everything
class UserManager {
createUser() {}
deleteUser() {}
updateUser() {}
sendEmail() {}
generateReport() {}
processPayment() {}
uploadAvatar() {}
validatePermissions() {}
}
// β
Good: Single responsibility classes
class UserService {
createUser() {}
deleteUser() {}
updateUser() {}
}
class UserNotificationService {
sendEmail() {}
sendSMS() {}
}
class UserReportService {
generateReport() {}
}
class UserPaymentService {
processPayment() {}
}
// Code Smell: Magic Numbers
// β Bad
if (user.age > 18) {
// ...
}
setTimeout(() => {}, 3600000);
// β
Good
const MINIMUM_AGE = 18;
if (user.age > MINIMUM_AGE) {
// ...
}
const ONE_HOUR_MS = 60 * 60 * 1000;
setTimeout(() => {}, ONE_HOUR_MS);
// Code Smell: Nested Conditionals
// β Bad: Deep nesting
function getDiscount(user) {
if (user) {
if (user.isActive) {
if (user.orders.length > 10) {
if (user.totalSpent > 1000) {
return 0.2;
} else {
return 0.1;
}
} else {
return 0.05;
}
}
}
return 0;
}
// β
Good: Early returns
function getDiscount(user) {
if (!user || !user.isActive) return 0;
if (user.orders.length <= 10) return 0.05;
if (user.totalSpent > 1000) return 0.2;
return 0.1;
}
4. Function Best Practices
// Keep functions small (< 20 lines)
// Do one thing well
// Use descriptive names
// Minimize parameters (< 3 preferred)
// No side effects
// β Bad: Side effects
let total = 0;
function addToTotal(amount) {
total += amount; // Modifies external state
return total;
}
// β
Good: Pure function
function calculateTotal(currentTotal, amount) {
return currentTotal + amount;
}
// β Bad: Multiple responsibilities
function saveUserAndSendEmail(user) {
db.save(user);
emailService.send(user.email, 'Welcome!');
}
// β
Good: Separate concerns
function saveUser(user) {
return db.save(user);
}
function sendWelcomeEmail(user) {
return emailService.send(user.email, 'Welcome!');
}
// Combine in orchestrator
async function registerUser(user) {
await saveUser(user);
await sendWelcomeEmail(user);
}
// Use default parameters
// β Bad
function createUser(name, role) {
role = role || 'user';
// ...
}
// β
Good
function createUser(name, role = 'user') {
// ...
}
// Destructure parameters
// β Bad
function updateUser(user) {
const name = user.name;
const email = user.email;
const age = user.age;
// ...
}
// β
Good
function updateUser({ name, email, age }) {
// ...
}
// Return early
// β Bad
function isValidUser(user) {
let valid = false;
if (user) {
if (user.email) {
if (user.password) {
valid = true;
}
}
}
return valid;
}
// β
Good
function isValidUser(user) {
if (!user) return false;
if (!user.email) return false;
if (!user.password) return false;
return true;
}
// Or even better
function isValidUser(user) {
return user && user.email && user.password;
}
5. Comments & Documentation
// Good comments explain WHY, not WHAT
// β Bad: Comments stating the obvious
// Increment i
i++;
// Loop through users
for (const user of users) {
// ...
}
// β
Good: Explain business logic or reasoning
// Retry 3 times because API is occasionally unreliable
const MAX_RETRIES = 3;
// Use exponential backoff to avoid overwhelming the server
const delay = Math.pow(2, attemptNumber) * 1000;
// Complex algorithms should be explained
/**
* Calculates shipping cost using tiered pricing model:
* - First 5kg: $10
* - 5-20kg: $2 per kg
* - Over 20kg: $1.50 per kg
*/
function calculateShippingCost(weight) {
if (weight <= 5) return 10;
if (weight <= 20) return 10 + (weight - 5) * 2;
return 10 + 15 * 2 + (weight - 20) * 1.5;
}
// JSDoc for public APIs
/**
* Creates a new user account
* @param {Object} userData - User information
* @param {string} userData.name - Full name
* @param {string} userData.email - Email address
* @param {string} userData.password - Password (min 8 chars)
* @returns {Promise} Created user object
* @throws {ValidationError} If email is invalid
*/
async function createUser(userData) {
// Implementation
}
// TODO comments for future work
// TODO: Add caching layer for frequently accessed users
// FIXME: This breaks when user has special characters in name
// HACK: Temporary workaround until API v2 is released
// Self-documenting code is better than comments
// β Bad
// Check if user is adult
if (user.age >= 18) {
// ...
}
// β
Good
function isAdult(user) {
const ADULT_AGE = 18;
return user.age >= ADULT_AGE;
}
if (isAdult(user)) {
// ...
}
6. Error Handling
// Use custom error classes
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
class NotFoundError extends Error {
constructor(resource, id) {
super(`${resource} with id ${id} not found`);
this.name = 'NotFoundError';
this.resource = resource;
this.id = id;
}
}
// Fail fast
function processPayment(amount, currency) {
if (!amount || amount <= 0) {
throw new ValidationError('Amount must be positive', 'amount');
}
if (!currency) {
throw new ValidationError('Currency is required', 'currency');
}
// Process payment
}
// Handle errors at appropriate level
async function getUser(id) {
try {
const user = await db.findOne('users', { id });
if (!user) {
throw new NotFoundError('User', id);
}
return user;
} catch (error) {
// Log error but let it propagate
logger.error('Error fetching user', { id, error });
throw error;
}
}
// Express error handling
app.use((err, req, res, next) => {
if (err instanceof ValidationError) {
return res.status(400).json({
error: err.message,
field: err.field
});
}
if (err instanceof NotFoundError) {
return res.status(404).json({
error: err.message
});
}
// Unknown error
logger.error('Unhandled error', { error: err });
res.status(500).json({
error: 'Internal server error'
});
});
// Don't catch errors you can't handle
// β Bad: Swallowing errors
try {
await criticalOperation();
} catch (error) {
console.log('Error occurred');
// Error is lost!
}
// β
Good: Let it propagate or handle properly
try {
await criticalOperation();
} catch (error) {
logger.error('Critical operation failed', { error });
throw error; // Re-throw for upstream handling
}
7. Code Organization
// Organize by feature, not by type
// β Bad: Organized by type
src/
controllers/
userController.js
orderController.js
models/
User.js
Order.js
services/
userService.js
orderService.js
// β
Good: Organized by feature
src/
users/
user.model.js
user.service.js
user.controller.js
user.routes.js
orders/
order.model.js
order.service.js
order.controller.js
order.routes.js
// Keep related code together
// File: users/user.service.js
class UserService {
constructor(userRepository, emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
async createUser(userData) {
const user = await this.userRepository.create(userData);
await this.emailService.sendWelcome(user.email);
return user;
}
async deleteUser(userId) {
return this.userRepository.delete(userId);
}
}
// Separate configuration
// config/database.js
module.exports = {
host: process.env.DB_HOST,
port: process.env.DB_PORT,
database: process.env.DB_NAME
};
// config/email.js
module.exports = {
apiKey: process.env.SENDGRID_API_KEY,
fromEmail: 'noreply@example.com'
};
8. Best Practices
β Clean Code Best Practices:
- β Follow SOLID principles for maintainable design
- β Use meaningful and descriptive names
- β Keep functions small and focused (one responsibility)
- β Avoid deep nesting (max 3 levels)
- β Write pure functions without side effects
- β Use early returns to reduce complexity
- β Extract magic numbers to named constants
- β Prefer composition over inheritance
- β Write self-documenting code
- β Handle errors explicitly and appropriately
- β Use dependency injection for testability
- β Organize code by feature/domain
- β Refactor regularly to reduce technical debt
- β Follow consistent code style (use linters)
- β Write code for humans, not machines
Conclusion
Clean code is professional code. Apply SOLID principles, use meaningful names, keep functions small and focused, eliminate code smells through refactoring, and organize code logically. Clean code reduces bugs, improves maintainability, and makes collaboration easier. Remember: code is read far more often than it's written.
π‘ Pro Tip: Use the Boy Scout Rule: "Always leave the code better than you found it." When you touch existing code, take a moment to improve it slightlyβrename a confusing variable, extract a complex condition into a well-named function, or add a helpful comment. Small, continuous improvements compound over time. Also, set up automated tools like ESLint, Prettier, and SonarQube to enforce code quality standards and catch issues during development, not in code review.