COMP 305: Object-Oriented Software Design

University of San Diego, Fall 2025

SOLID Principles

SOLID is an acronym for five design principles intended to make object-oriented designs more understandable, flexible, and maintainable. These principles were introduced by Robert C. Martin (Uncle Bob) and are fundamental to writing clean, scalable code.

S

Single Responsibility

A class should have one, and only one, reason to change

O

Open/Closed

Open for extension, closed for modification

L

Liskov Substitution

Subtypes must be substitutable for their base types

I

Interface Segregation

Many specific interfaces are better than one general

D

Dependency Inversion

Depend on abstractions, not concretions

1. Single Responsibility Principle (SRP)

"A class should have one, and only one, reason to change."

Each class should focus on a single task or responsibility. This makes code easier to understand, test, and maintain.

Violation Example

// ❌ VIOLATES SRP - User class has too many responsibilities
class User {
    constructor(name, email) {
        this.name = name;
        this.email = email;
    }

    // User data responsibility
    getName() {
        return this.name;
    }

    // Email validation responsibility
    validateEmail() {
        return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.email);
    }

    // Database responsibility
    save() {
        database.insert('users', this);
    }

    // Email sending responsibility
    sendWelcomeEmail() {
        emailService.send(this.email, 'Welcome!');
    }
}

Following SRP

// ✓ FOLLOWS SRP - Each class has one responsibility
class User {
    constructor(name, email) {
        this.name = name;
        this.email = email;
    }

    getName() {
        return this.name;
    }

    getEmail() {
        return this.email;
    }
}

class EmailValidator {
    validate(email) {
        return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
    }
}

class UserRepository {
    save(user) {
        database.insert('users', user);
    }
}

class EmailService {
    sendWelcome(user) {
        this.send(user.getEmail(), 'Welcome!');
    }
}

Benefits:

2. Open/Closed Principle (OCP)

"Software entities should be open for extension, but closed for modification."

You should be able to add new functionality without changing existing code. Use abstraction and polymorphism.

Violation Example

// ❌ VIOLATES OCP - Must modify class to add new discount types
class DiscountCalculator {
    calculate(customer, amount) {
        if (customer.type === 'regular') {
            return amount * 0.1;
        } else if (customer.type === 'premium') {
            return amount * 0.2;
        } else if (customer.type === 'vip') {
            return amount * 0.3;
        }
        // Adding new type requires modifying this class
    }
}

Following OCP

// ✓ FOLLOWS OCP - Open for extension, closed for modification
class DiscountStrategy {
    calculate(amount) {
        throw new Error('Must implement calculate method');
    }
}

class RegularDiscount extends DiscountStrategy {
    calculate(amount) {
        return amount * 0.1;
    }
}

class PremiumDiscount extends DiscountStrategy {
    calculate(amount) {
        return amount * 0.2;
    }
}

class VIPDiscount extends DiscountStrategy {
    calculate(amount) {
        return amount * 0.3;
    }
}

class Customer {
    constructor(discountStrategy) {
        this.discountStrategy = discountStrategy;
    }

    getDiscount(amount) {
        return this.discountStrategy.calculate(amount);
    }
}

// Easy to add new discount types without modifying existing code

Benefits:

3. Liskov Substitution Principle (LSP)

"Objects of a superclass should be replaceable with objects of a subclass without breaking the application."

Subtypes must be substitutable for their base types without altering correctness. Child classes should extend, not break, parent behavior.

Example

// ❌ VIOLATES LSP - Square breaks Rectangle's 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; // Breaks expected behavior!
    }

    setHeight(height) {
        this.width = height;
        this.height = height; // Breaks expected behavior!
    }
}

// ✓ FOLLOWS LSP - Proper abstraction
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;
    }
}

Benefits:

4. Interface Segregation Principle (ISP)

"No client should be forced to depend on methods it does not use."

Create specific interfaces rather than one general-purpose interface. Classes should only implement methods they need.

Violation Example

// ❌ VIOLATES ISP - Fat interface forces unnecessary implementations
class Worker {
    work() {}
    eat() {}
    sleep() {}
}

class HumanWorker extends Worker {
    work() { console.log('Working...'); }
    eat() { console.log('Eating...'); }
    sleep() { console.log('Sleeping...'); }
}

class RobotWorker extends Worker {
    work() { console.log('Working...'); }
    eat() { /* Robots don't eat! */ }
    sleep() { /* Robots don't sleep! */ }
}

Following ISP

// ✓ FOLLOWS ISP - Segregated interfaces
class Workable {
    work() {
        throw new Error('Must implement work');
    }
}

class Eatable {
    eat() {
        throw new Error('Must implement eat');
    }
}

class Sleepable {
    sleep() {
        throw new Error('Must implement sleep');
    }
}

class HumanWorker {
    work() { console.log('Working...'); }
    eat() { console.log('Eating...'); }
    sleep() { console.log('Sleeping...'); }
}

class RobotWorker {
    work() { console.log('Working...'); }
    // Only implements what it needs!
}

Benefits:

5. Dependency Inversion Principle (DIP)

"Depend upon abstractions, not concretions."

High-level modules should not depend on low-level modules. Both should depend on abstractions. This is often implemented through dependency injection.

Violation Example

// ❌ VIOLATES DIP - High-level depends on low-level concrete class
class MySQLDatabase {
    save(data) {
        console.log('Saving to MySQL:', data);
    }
}

class UserService {
    constructor() {
        this.database = new MySQLDatabase(); // Tight coupling!
    }

    createUser(userData) {
        this.database.save(userData);
    }
}

Following DIP

// ✓ FOLLOWS DIP - Depend on abstractions
class Database {
    save(data) {
        throw new Error('Must implement save');
    }
}

class MySQLDatabase extends Database {
    save(data) {
        console.log('Saving to MySQL:', data);
    }
}

class MongoDBDatabase extends Database {
    save(data) {
        console.log('Saving to MongoDB:', data);
    }
}

class UserService {
    constructor(database) {
        this.database = database; // Dependency injected!
    }

    createUser(userData) {
        this.database.save(userData);
    }
}

// Usage - easy to swap implementations
const mysqlService = new UserService(new MySQLDatabase());
const mongoService = new UserService(new MongoDBDatabase());

Benefits:

Why SOLID Matters

When to Apply SOLID

SOLID principles are guidelines, not absolute rules. Consider:

Resources