Home / Notebooks / Software Engineering
Software Engineering
intermediate

Clean Code Principles

Essential principles and practices for writing clean, maintainable code

March 10, 2024
•
Updated regularly

Clean Code Principles

Quick reference guide for writing clean, maintainable code.

What is Clean Code?

Clean code is code that is easy to understand and easy to change:

  • Readable: Clear intent and logic
  • Simple: Does one thing well
  • Tested: Verified to work correctly
  • Maintainable: Easy to modify and extend
  • Efficient: Performs well without premature optimization
  • > "Any fool can write code that a computer can understand. Good programmers write code that humans can understand." - Martin Fowler

    Meaningful Names

    Use intention-revealing names that explain what something does:

    Bad Examples

    // ❌ BAD: Unclear, abbreviated, meaningless
    let d; // elapsed time in days
    let list1;
    function getData() {}
    const a = 5;
    let temp;
    

    Good Examples

    // ✅ GOOD: Clear, descriptive, purposeful
    let elapsedTimeInDays;
    let activeUsers;
    function getUserProfile() {}
    const MAX_LOGIN_ATTEMPTS = 5;
    let userTemporaryPassword;
    

    Naming Rules

    // ========== Use Pronounceable Names ==========
    // ❌ BAD
    const yyyymmdstr = moment().format("YYYY/MM/DD");
    
    // ✅ GOOD
    const currentDate = moment().format("YYYY/MM/DD");
    
    // ========== Use Searchable Names ==========
    // ❌ BAD
    setTimeout(doSomething, 86400000); // What is 86400000?
    
    // ✅ GOOD
    const MILLISECONDS_IN_A_DAY = 86400000;
    setTimeout(doSomething, MILLISECONDS_IN_A_DAY);
    
    // ========== Avoid Mental Mapping ==========
    // ❌ BAD
    locations.forEach((l) => {
      doStuff();
      doSomeOtherStuff();
      // Wait, what is `l` again?
      dispatch(l);
    });
    
    // ✅ GOOD
    locations.forEach((location) => {
      doStuff();
      doSomeOtherStuff();
      dispatch(location);
    });
    

    Functions

    Functions should be small and do one thing:

    Function Size

    // ❌ BAD: Too long, does multiple things
    function processUser(user) {
      // Validate user
      if (!user.email) return false;
      if (!user.name) return false;
      
      // Save to database
      const result = db.save(user);
      
      // Send email
      sendEmail(user.email, "Welcome!");
      
      // Log activity
      logger.info("User processed");
      
      // Update cache
      cache.set(user.id, user);
      
      return result;
    }
    
    // ✅ GOOD: Small, single responsibility
    function validateUser(user) {
      return user.email && user.name;
    }
    
    function saveUser(user) {
      return db.save(user);
    }
    
    function notifyUser(user) {
      sendEmail(user.email, "Welcome!");
    }
    
    function logUserActivity(action) {
      logger.info(`User ${action}`);
    }
    
    function processUser(user) {
      if (!validateUser(user)) return false;
      
      const result = saveUser(user);
      notifyUser(user);
      logUserActivity("processed");
      
      return result;
    }
    

    Function Arguments

    // ❌ BAD: Too many arguments
    function createUser(name, email, age, country, city, phone, address) {
      // ...
    }
    
    // ✅ GOOD: Use object parameter
    function createUser(userDetails) {
      const { name, email, age, country, city, phone, address } = userDetails;
      // ...
    }
    
    // ========== Avoid Flag Arguments ==========
    // ❌ BAD
    function createFile(name, isPublic) {
      if (isPublic) {
        // create public file
      } else {
        // create private file
      }
    }
    
    // ✅ GOOD
    function createPublicFile(name) {
      // create public file
    }
    
    function createPrivateFile(name) {
      // create private file
    }
    

    Descriptive Function Names

    // ❌ BAD: Vague names
    function process() {}
    function handle() {}
    function do() {}
    
    // ✅ GOOD: Clear action and purpose
    function processPayment() {}
    function handleUserLogin() {}
    function validateEmailFormat() {}
    function calculateTotalPrice() {}
    function sendWelcomeEmail() {}
    

    Don't Repeat Yourself (DRY)

    Eliminate duplication by extracting common code:

    // ❌ BAD: Duplicated code
    function calculateDiscountForStudent(price) {
      const discount = price * 0.1;
      return price - discount;
    }
    
    function calculateDiscountForSenior(price) {
      const discount = price * 0.15;
      return price - discount;
    }
    
    function calculateDiscountForMilitary(price) {
      const discount = price * 0.2;
      return price - discount;
    }
    
    // ✅ GOOD: Reusable function
    function calculateDiscount(price, discountRate) {
      const discount = price * discountRate;
      return price - discount;
    }
    
    const DISCOUNT_RATES = {
      STUDENT: 0.1,
      SENIOR: 0.15,
      MILITARY: 0.2
    };
    
    calculateDiscount(100, DISCOUNT_RATES.STUDENT);
    calculateDiscount(100, DISCOUNT_RATES.SENIOR);
    

    Comments

    Code should be self-explanatory. Use comments only when necessary:

    Bad Comments

    // ❌ BAD: Obvious, redundant comments
    // Get the user
    const user = getUser();
    
    // Loop through items
    items.forEach(item => {
      // Process the item
      process(item);
    });
    
    // Increment i
    i++;
    

    Good Comments

    // ✅ GOOD: Explain WHY, not WHAT
    // Using regex instead of string methods for 10x performance improvement
    const isValid = /^[a-z0-9]+$/i.test(input);
    
    // Workaround for IE11 bug where Date.now() returns incorrect value
    const timestamp = Date.now() || new Date().getTime();
    
    // TODO: Refactor this when API v2 is ready
    // WARNING: This operation is expensive, consider caching
    // HACK: Temporary fix for legacy system, remove after migration
    

    Self-Documenting Code

    // ❌ BAD: Needs comment to explain
    // Check if user has permission
    if (user.role === 'admin' || user.role === 'moderator') {
      // Allow access
    }
    
    // ✅ GOOD: Code explains itself
    function hasModeratorPermission(user) {
      const moderatorRoles = ['admin', 'moderator'];
      return moderatorRoles.includes(user.role);
    }
    
    if (hasModeratorPermission(user)) {
      // Allow access
    }
    

    Error Handling

    Handle errors gracefully and consistently:

    // ❌ BAD: Silent failures
    function getUser(id) {
      try {
        return db.findUser(id);
      } catch (error) {
        return null; // What went wrong?
      }
    }
    
    // ✅ GOOD: Descriptive error handling
    function getUser(id) {
      try {
        return db.findUser(id);
      } catch (error) {
        logger.error(`Failed to fetch user ${id}:`, error);
        throw new UserNotFoundError(`User with ID ${id} not found`);
      }
    }
    
    // ========== Use Custom Errors ==========
    class ValidationError extends Error {
      constructor(message) {
        super(message);
        this.name = 'ValidationError';
      }
    }
    
    class DatabaseError extends Error {
      constructor(message) {
        super(message);
        this.name = 'DatabaseError';
      }
    }
    
    // ========== Handle Different Error Types ==========
    try {
      processPayment(order);
    } catch (error) {
      if (error instanceof ValidationError) {
        return res.status(400).json({ error: error.message });
      }
      if (error instanceof DatabaseError) {
        return res.status(500).json({ error: 'Database error occurred' });
      }
      throw error; // Unexpected error, rethrow
    }
    

    Code Organization

    Organize code logically and consistently:

    File Structure

    src/
    ├── components/          # UI components
    │   ├── Button/
    │   │   ├── Button.tsx
    │   │   ├── Button.test.tsx
    │   │   └── index.ts
    │   └── Card/
    ├── services/            # Business logic
    │   ├── userService.ts
    │   └── paymentService.ts
    ├── utils/               # Utility functions
    │   ├── formatters.ts
    │   └── validators.ts
    ├── types/               # Type definitions
    └── constants/           # App constants
    

    Code Grouping

    // ✅ GOOD: Group related code together
    class UserService {
      // Constants
      private readonly MAX_ATTEMPTS = 3;
      private readonly CACHE_TTL = 3600;
      
      // Constructor
      constructor(private db: Database) {}
      
      // Public methods
      async getUser(id: string) {
        return this.fetchUserFromCache(id) || this.fetchUserFromDB(id);
      }
      
      async createUser(data: UserData) {
        this.validateUserData(data);
        return this.saveUser(data);
      }
      
      // Private helper methods
      private async fetchUserFromCache(id: string) {
        // ...
      }
      
      private async fetchUserFromDB(id: string) {
        // ...
      }
      
      private validateUserData(data: UserData) {
        // ...
      }
    }
    

    SOLID Principles

    Single Responsibility Principle (SRP)

    A class should have one reason to change:

    // ❌ BAD: Multiple responsibilities
    class User {
      constructor(name, email) {
        this.name = name;
        this.email = email;
      }
      
      save() {
        // Database logic
        db.save(this);
      }
      
      sendEmail(message) {
        // Email logic
        emailService.send(this.email, message);
      }
      
      generateReport() {
        // Reporting logic
        return `Report for ${this.name}`;
      }
    }
    
    // ✅ GOOD: Single responsibility
    class User {
      constructor(name, email) {
        this.name = name;
        this.email = email;
      }
    }
    
    class UserRepository {
      save(user) {
        db.save(user);
      }
    }
    
    class EmailService {
      sendToUser(user, message) {
        emailService.send(user.email, message);
      }
    }
    
    class ReportGenerator {
      generateUserReport(user) {
        return `Report for ${user.name}`;
      }
    }
    

    Open/Closed Principle (OCP)

    Open for extension, closed for modification:

    // ❌ BAD: Need to modify for new types
    class PaymentProcessor {
      process(payment, type) {
        if (type === 'credit') {
          // Process credit card
        } else if (type === 'paypal') {
          // Process PayPal
        } else if (type === 'bitcoin') {
          // Process Bitcoin
        }
      }
    }
    
    // ✅ GOOD: Extend without modifying
    class PaymentProcessor {
      constructor(private paymentMethod: PaymentMethod) {}
      
      process(payment) {
        this.paymentMethod.processPayment(payment);
      }
    }
    
    class CreditCardPayment implements PaymentMethod {
      processPayment(payment) {
        // Process credit card
      }
    }
    
    class PayPalPayment implements PaymentMethod {
      processPayment(payment) {
        // Process PayPal
      }
    }
    

    Dependency Inversion Principle (DIP)

    Depend on abstractions, not concretions:

    // ❌ BAD: Direct dependency on concrete class
    class UserService {
      constructor() {
        this.database = new MySQLDatabase(); // Tightly coupled
      }
      
      getUser(id) {
        return this.database.find(id);
      }
    }
    
    // ✅ GOOD: Depend on interface/abstraction
    class UserService {
      constructor(private database: Database) {} // Injected dependency
      
      getUser(id) {
        return this.database.find(id);
      }
    }
    
    // Can easily swap implementations
    const mysqlDb = new MySQLDatabase();
    const mongoDb = new MongoDatabase();
    const userService = new UserService(mysqlDb); // or mongoDb
    

    Testing

    Write testable code with clear test cases:

    // ✅ GOOD: Small, testable functions
    function calculateTotalPrice(items, discountRate = 0) {
      const subtotal = items.reduce((sum, item) => sum + item.price, 0);
      const discount = subtotal * discountRate;
      return subtotal - discount;
    }
    
    // Test cases
    describe('calculateTotalPrice', () => {
      it('should calculate total without discount', () => {
        const items = [{ price: 10 }, { price: 20 }];
        expect(calculateTotalPrice(items)).toBe(30);
      });
      
      it('should apply discount correctly', () => {
        const items = [{ price: 100 }];
        expect(calculateTotalPrice(items, 0.1)).toBe(90);
      });
      
      it('should handle empty items', () => {
        expect(calculateTotalPrice([])).toBe(0);
      });
    });
    

    Code Smells to Avoid

    Long Methods

    // ❌ BAD: Method doing too much
    function processOrder(order) {
      // 100+ lines of code
    }
    
    // ✅ GOOD: Break into smaller functions
    function processOrder(order) {
      validateOrder(order);
      calculateTotal(order);
      applyDiscount(order);
      processPayment(order);
      sendConfirmation(order);
    }
    

    Magic Numbers

    // ❌ BAD: Unexplained numbers
    if (user.age > 18 && user.status === 2) {
      // ...
    }
    
    // ✅ GOOD: Named constants
    const LEGAL_AGE = 18;
    const STATUS_ACTIVE = 2;
    
    if (user.age > LEGAL_AGE && user.status === STATUS_ACTIVE) {
      // ...
    }
    

    Deep Nesting

    // ❌ BAD: Deeply nested
    function processUser(user) {
      if (user) {
        if (user.isActive) {
          if (user.hasPermission) {
            if (user.email) {
              // Do something
            }
          }
        }
      }
    }
    
    // ✅ GOOD: Early returns
    function processUser(user) {
      if (!user) return;
      if (!user.isActive) return;
      if (!user.hasPermission) return;
      if (!user.email) return;
      
      // Do something
    }
    

    Best Practices

    Consistent Formatting

    // ✅ GOOD: Consistent style
    const users = [
      { name: 'Alice', age: 25 },
      { name: 'Bob', age: 30 },
      { name: 'Charlie', age: 35 },
    ];
    
    function getActiveUsers(users) {
      return users.filter(user => user.isActive);
    }
    

    Meaningful Variable Names

    // ❌ BAD
    const x = users.filter(u => u.a > 18);
    
    // ✅ GOOD
    const adultUsers = users.filter(user => user.age > 18);
    

    Pure Functions

    // ❌ BAD: Has side effects
    let total = 0;
    function addToTotal(value) {
      total += value; // Modifies external state
    }
    
    // ✅ GOOD: Pure function
    function calculateTotal(currentTotal, value) {
      return currentTotal + value; // No side effects
    }
    

    Use Modern Language Features

    // ❌ BAD: Old style
    var result = [];
    for (var i = 0; i < items.length; i++) {
      if (items[i].active) {
        result.push(items[i].name);
      }
    }
    
    // ✅ GOOD: Modern approach
    const result = items
      .filter(item => item.active)
      .map(item => item.name);
    

    Tips

  • Write code for humans first, computers second
  • Refactor frequently to keep code clean
  • Test your code to ensure it works as expected
  • Use meaningful names for variables, functions, and classes
  • Keep functions small and focused on one task
  • Avoid premature optimization - make it work first, then optimize
  • Delete dead code instead of commenting it out
  • Be consistent with your team's style guide
  • Review your own code before asking others to review it
  • Continuous improvement - always look for ways to improve
  • Resources

  • Clean Code by Robert C. Martin
  • Refactoring by Martin Fowler
  • Code Complete by Steve McConnell
  • The Pragmatic Programmer
  • Clean Code JavaScript
  • Topics

    Clean CodeBest PracticesSoftware EngineeringCode Quality

    Found This Helpful?

    If you have questions or suggestions for improving these notes, I'd love to hear from you.