DEV Community

Minuth Prom
Minuth Prom

Posted on

Clean Code and Software Design Principles

Essential practices for writing maintainable, scalable, and collaborative software.

1. Meaningful Names

  • Use descriptive, unambiguous names for variables, functions, classes, and modules.
  • Use consistent formatting (e.g., camelCase, PascalCase, snake_case).

Example:

// Bad
const d = new Date();
const usr = getUsr();
function calc(a: number, b: number): number { return a * b * 0.15; }

// Good
const currentDate = new Date();
const activeUser = getActiveUser();
function calculateTaxAmount(price: number, quantity: number): number {
  const TAX_RATE = 0.15;
  return price * quantity * TAX_RATE;
}
Enter fullscreen mode Exit fullscreen mode

2. Functions

  • Focus on a single task.
  • Use names that describe the function's purpose.
  • Avoid excessive nested functions.
  • Use object parameters if the function has many parameters.

Example:

// Bad
class UserService {
  registerUser(user: User) {
    this.validate(user);
  }

  private validate(user: User) {
    if (!user.email) {
      throw new Error("Email required");
    }
    this.save(user);
  }

  private save(user: User) {
    // pretend DB save
    console.log("Saving user:", user.name);
    this.sendWelcome(user);
  }

  private sendWelcome(user: User) {
    console.log("Sending welcome email to:", user.email);
  }
}
// Good
class UserService {
  registerUser(user: User) {
    this.validate(user);
    this.save(user);
    this.sendWelcome(user);
  }

  private validate(user: User) {
    if (!user.email) {
      throw new Error("Email required");
    }
  }

  private save(user: User) {
    // pretend DB save
    console.log("Saving user:", user.name);
  }

  private sendWelcome(user: User) {
    console.log("Sending welcome email to:", user.email);
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Comments

  • Use comments to explain unclear code.

4. Flat is better than nested

  • Avoid nested structures that make your code difficult to read. Example:
// Bad - Nested conditions
function validateUser(user: User) {
  if (user) {
    if (user.email) {
      if (user.age >= 18) {
        return true;
      } else {
        return false;
      }
    } else {
      return false;
    }
  } else {
    return false;
  }
}
// Good - Early returns to reduce nesting
function validateUser(user: User) {
  if (!user) return false;
  if (!user.email) return false;
  if (user.age < 18) return false;
  return true;
}
Enter fullscreen mode Exit fullscreen mode

5. SOLID Principles

  • Single Responsibility Principle (SRP): A class or function should have only one responsibility. Example:
// Bad - Single class with multiple responsibilities
class UserManager {
  // User-related responsibility
  createUser(name: string, email: string) {
    console.log(`User created: ${name}, ${email}`);
  }

  // Unrelated responsibility: file operations
  writeFile(content: string) {
    console.log(`Writing file with content: ${content}`);
  }

  // Unrelated responsibility: math utility
  calculateTax(amount: number) {
    return amount * 0.1;
  }
}

// Good - Separate classes for each responsibility
class UserService {
  createUser(name: string, email: string) {
    console.log(`User created: ${name}, ${email}`);
  }
}
class FileService {
  writeFile(content: string) {
    console.log(`Writing file with content: ${content}`);
  }
}
class TaxCalculator {
  calculateTax(amount: number) {
    return amount * 0.1;
  }
}
Enter fullscreen mode Exit fullscreen mode
  • Open/Closed Principle (OCP): Open for extension, closed for modification. Example:
// Bad - Modifying existing code to add new shapes
class AreaCalculator {
    calculateArea(shape: any): number {
        if (shape.type === "circle") {
            return Math.PI * shape.radius * shape.radius;
        } else if (shape.type === "rectangle") {
            return shape.width * shape.height;
        }
        // Adding new shapes requires modifying this method
    }
}

// Good - Extending functionality without modifying existing code
interface Shape {
    area(): number;
}
class Circle implements Shape {
    constructor(private radius: number) {}
    area(): number {
        return Math.PI * this.radius * this.radius;
    }
}
class Rectangle implements Shape {
    constructor(private width: number, private height: number) {}
    area(): number {
        return this.width * this.height;
    }
}
class AreaCalculator {
    calculateArea(shape: Shape): number {
        return shape.area();
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Liskov Substitution Principle (LSP): Derived classes must be substitutable for base classes. Example:
// Bad - Subclass that violates LSP
class Bird {
    fly(): void {
        console.log("Flying");
    }
}
class Ostrich extends Bird {
    fly(): void {
        throw new Error("Ostriches can't fly!");
    }
}
// Good - Separate hierarchy for non-flying birds
class Bird {
    eat(): void {
        console.log("Eating");
    }
}

class FlyingBird extends Bird {
    fly(): void {
        console.log("Flying");
    }
}
class Ostrich extends Bird {
    // Ostrich does not have fly method
}

Enter fullscreen mode Exit fullscreen mode
  • Interface Segregation Principle (ISP): Clients should not be forced to depend on interfaces they don't use. Example:
// Bad - Fat interface
interface Worker {
    work(): void;
    eat(): void;
}
class HumanWorker implements Worker {
    work(): void {
        console.log("Working");
    }
    eat(): void {
        console.log("Eating");
    }
}
class RobotWorker implements Worker {   
    work(): void {
        console.log("Working");
    }
    // Robot does not eat, but forced to implement eat()
    eat(): void {
        throw new Error("Robots don't eat!");
    }
}

// Good - Segregated interfaces
interface Workable {
    work(): void;
}
interface Eatable {
    eat(): void;
}
class HumanWorker implements Workable, Eatable {
    work(): void {
        console.log("Working");
    }
    eat(): void {
        console.log("Eating");
    }
}
class RobotWorker implements Workable {   
    work(): void {
        console.log("Working");
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Dependency Inversion Principle (DIP): Depend on abstractions, not concretions. Example:
// Bad - High-level module depends on low-level module
class MySQLDatabase {
    connect(): void {
        console.log("Connected to MySQL");
    }
}
class UserService {
    private database: MySQLDatabase;
    constructor() {
        this.database = new MySQLDatabase();
    }
    getUser(): void {
        this.database.connect();
        console.log("Fetching user");
    }
}

// Good - Both high-level and low-level modules depend on abstractions
interface Database {
    connect(): void;
}
class MySQLDatabase implements Database {
    connect(): void {
        console.log("Connected to MySQL");
    }
}
class PostgreSQLDatabase implements Database {
    connect(): void {
        console.log("Connected to PostgreSQL");
    }
}
class UserService {
    private database: Database;
    constructor(database: Database) {
        this.database = database;
    }
    getUser(): void {
        this.database.connect();
        console.log("Fetching user");
    }
}
// Usage
new UserService(new MySQLDatabase())
new UserService(new PostgreSQLDatabase())
Enter fullscreen mode Exit fullscreen mode

6. Clean Architecture

  • Layer Separation: Separate layers by role (e.g., Service, Repository, Controller).
  • Dependency Rule: Follow the Dependency Inversion Principle to ensure high-level modules don't depend on low-level modules.
  • Independence: Business logic should not depend on frameworks, UI, or databases.
  • Testability: Easy to test without external dependencies.

Example (Layered Architecture):

// Domain Layer
class User {
    constructor(public id: number, public name: string) {}
}
interface UserRepository {
    getUserById(id: number): User;
}

class InMemoryUserRepository implements UserRepository {
    private users: User[] = [new User(1, "Alice"), new User(2, "Bob")];
    getUserById(id: number): User {
        return this.users.find(user => user.id === id);
    }
}

// Application Layer
class UserService {
    constructor(private userRepository: UserRepository) {}
    getUserProfile(id: number): User {
        return this.userRepository.getUserById(id);
    }
}

class UserController {
    constructor(private userService: UserService) {}
    getUserProfile(id: number): void {
        const user = this.userService.getUserProfile(id);
        console.log(`User Profile: ${user.name}`);
    }
}
// Usage
const userRepository = new InMemoryUserRepository();
const userService = new UserService(userRepository);
const userController = new UserController(userService);
userController.getUserProfile(1);
Enter fullscreen mode Exit fullscreen mode

Top comments (0)