DEV Community

Cover image for The SOLID Principles for Writing Scalable & Maintainable Code
Dipak Ahirav
Dipak Ahirav

Posted on

The SOLID Principles for Writing Scalable & Maintainable Code

Introduction

Writing clean, maintainable, and scalable code is essential in the fast-evolving world of software development. The SOLID principles, devised by Robert C. Martin, provide a set of guidelines that help developers achieve this through better software design. Let's explore these principles with JavaScript-specific examples.

S: Single Responsibility Principle (SRP)

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

Explanation: SRP suggests that a class or module should have one responsibility and thus only one reason to change. This approach simplifies system modification and testing.

Example:
Consider a module that handles user data and generates user reports:

class UserHandler {
    constructor(user) {
        this.user = user;
    }

    saveUser() {
        console.log(`Saving user ${this.user.name}`);
        // logic to save user to a database
    }

    generateReport() {
        console.log(`Generating report for ${this.user.name}`);
        // logic to generate a user report
    }
}
Enter fullscreen mode Exit fullscreen mode

This class violates SRP because it manages user data and also handles report generation. We can refactor it into two classes:

class UserStorage {
    constructor(user) {
        this.user = user;
    }

    saveUser() {
        console.log(`Saving user ${this.user.name}`);
        // logic to save user to a database
    }
}

class UserReport {
    constructor(user) {
        this.user = user;
    }

    generateReport() {
        console.log(`Generating report for ${this.user.name}`);
        // logic to generate a user report
    }
}
Enter fullscreen mode Exit fullscreen mode

O: Open/Closed Principle (OCP)

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

Explanation: This principle advocates that classes should be extendable without modifying the existing code, thus preventing changes in existing code and associated risks.

Example:
Imagine a basic payment class that needs to be extended to support different payment methods:

class Payment {
    process(amount) {
        console.log(`Processing ${amount} via Credit`);
    }
}

// Extending Payment for new payment method
class PaymentWithDebit extends Payment {
    process(amount) {
        console.log(`Processing ${amount} via Debit`);
    }
}
Enter fullscreen mode Exit fullscreen mode

This setup allows new payment methods to be added without altering existing classes, complying with OCP.

L: Liskov Substitution Principle (LSP)

Definition: Subtypes must be substitutable for their base types.

Explanation: According to LSP, if a program uses a base class, the reference to the base class can be replaced with a derived class without affecting the program's functionality.

Example:
Suppose we have a rectangle class and a square class:

class Rectangle {
    constructor(width, height) {
        this.width = width;
        this.height = height;
    }

    setWidth(width) {
        this.width = width;
    }

    setHeight(height) {
        this.height = height;
    }

    area() {
        return this.width * this.height;
    }
}

class Square extends Rectangle {
    setWidth(width) {
        super.setWidth(width);
        super.setHeight(width);
    }

    setHeight(height) {
        super.setWidth(height);
        super.setHeight(height);
    }
}
Enter fullscreen mode Exit fullscreen mode

In this scenario, Square should not inherit from Rectangle as it changes the behavior of setWidth and setHeight methods, violating LSP. Each should have its own class if their behaviors differ significantly.

I: Interface Segregation Principle (ISP)

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

Explanation: ISP advocates for creating specific interfaces rather than general-purpose interfaces, so that implementing classes only need to be concerned with methods that are pertinent to them.

Example:
Consider an interface with too many responsibilities:

class Worker {
    work() {
        console.log('Working on tasks');
    }

    eat() {
        console.log('Lunch break');
    }

    train() {
        console.log('Training in progress');
    }
}

class Robot extends Worker {
    train() {
        // Irrelevant method for a robot
        throw new Error('Not applicable');
    }
}
Enter fullscreen mode Exit fullscreen mode

A better approach is to break down the Worker interface:

class Workable {
    work() {
        console.log('Working on tasks');
    }
}

class Eatable {
    eat() {
        console.log('Lunch break');
    }
}

class Trainable {
    train() {
        console.log('Training in progress');
    }
}

class HumanWorker extends Workable, Eatable, Trainable {}
class RobotWorker extends Workable

 {}
Enter fullscreen mode Exit fullscreen mode

D: Dependency Inversion Principle (DIP)

Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions.

Explanation: DIP suggests that high-level and low-level modules should interact through interfaces (abstractions), reducing the dependencies on concrete implementations.

Example:
Here's a basic example with tight coupling:

class ContentDatabase {
    store(content) {
        console.log(`Storing content: ${content}`);
        // Database storage logic
    }
}

class ContentService {
    constructor() {
        this.db = new ContentDatabase();
    }

    addContent(content) {
        this.db.store(content);
    }
}
Enter fullscreen mode Exit fullscreen mode

Refactored with dependency inversion:

class Database {
    store(content) {
        throw new Error('Method store() must be implemented');
    }
}

class ContentDatabase extends Database {
    store(content) {
        console.log(`Storing content: ${content}`);
        // Database storage logic
    }
}

class ContentService {
    constructor(database) {
        this.db = database;
    }

    addContent(content) {
        this.db.store(content);
    }
}
Enter fullscreen mode Exit fullscreen mode

This setup allows ContentService to be more flexible and easier to test, as it can work with any database that adheres to the Database interface.

Conclusion

Applying the SOLID principles in JavaScript projects can significantly enhance the maintainability, scalability, and robustness of your code. By ensuring that each component of your software adheres to these principles, you establish a strong foundation for a sustainable project.

Top comments (0)