DEV Community

Cover image for Understanding the SOLID Principles in JavaScript
Muhammad Tayyab Sheikh
Muhammad Tayyab Sheikh

Posted on

Understanding the SOLID Principles in JavaScript

When diving into the world of object-oriented programming (OOP), one will inevitably encounter the SOLID principles. These five principles, introduced by Robert C. Martin, guide developers in creating maintainable, scalable, and robust software designs. In this article, we'll break down each principle and provide practical JavaScript examples to illustrate them.

1. Single Responsibility Principle (SRP)

Concept:

Every module or class should have responsibility for a single piece of functionality. By adhering to this principle, the module or class becomes more robust and changes in one functionality don't impact others.

JavaScript Example:

Consider a class that manages user details and also handles user data storage:

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

    saveToDatabase() {
        // Logic to save user data to database
    }

    displayDetails() {
        console.log(`Name: ${this.user.name}, Email: ${this.user.email}`);
    }
}
Enter fullscreen mode Exit fullscreen mode

In the above design, the UserManager class is responsible for both managing user details and saving them to a database, violating the SRP. A better approach:

class User {
    constructor(name, email) {
        this.name = name;
        this.email = email;
    }

    displayDetails() {
        console.log(`Name: ${this.name}, Email: ${this.email}`);
    }
}

class UserDataBase {
    saveUser(user) {
        // Logic to save user data to database
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Open/Closed Principle (OCP)

Concept:

Software entities should be open for extension but closed for modification. This ensures that any new functionality can be added without changing existing code.

JavaScript Example:

Imagine we have a class that calculates the area of a rectangle:

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

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

class AreaCalculator {
    calculateArea(shape) {
        return shape.area();
    }
}
Enter fullscreen mode Exit fullscreen mode

What if we want to add a circle next? The current design requires modifying AreaCalculator. Instead, we can use polymorphism:

class Shape {
    area() {}
}

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

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

class Circle extends Shape {
    constructor(radius) {
        super();
        this.radius = radius;
    }

    area() {
        return 3.14 * this.radius * this.radius;
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, adding more shapes won't require changing AreaCalculator.

3. Liskov Substitution Principle (LSP)

Concept:

Derived classes must be substitutable for their base classes. This ensures that a derived class can replace its base class without affecting program behavior.

JavaScript Example:

Using our previous shapes, let's ensure that any new shape we add adheres to the base Shape contract:

class Triangle extends Shape {
    constructor(base, height) {
        super();
        this.base = base;
        this.height = height;
    }

    area() {
        if (this.base <= 0 || this.height <= 0) {
            throw new Error("Invalid dimensions");
        }
        return 0.5 * this.base * this.height;
    }
}
Enter fullscreen mode Exit fullscreen mode

This ensures that the derived Triangle class can replace the Shape class without issues.

4. Interface Segregation Principle (ISP)

Concept:

Clients shouldn't be forced to implement interfaces they don't use. By following this, we avoid bloating classes with unnecessary methods.

JavaScript Note:

JavaScript doesn't have interfaces like other languages, but we can mimic this concept using classes and methods.

Example:

If we have a Bird class, not all birds can swim:

class Bird {
    fly() {
        // Implementation
    }

    swim() {
        // Not all birds can swim!
    }
}
Enter fullscreen mode Exit fullscreen mode

A better approach:

class Bird {
    fly() {
        // Implementation
    }
}

class SwimmingBird extends Bird {
    swim() {
        // Now only birds that can swim will have this method
    }
}
Enter fullscreen mode Exit fullscreen mode

5. Dependency Inversion Principle (DIP)

Concept:

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

JavaScript Example:

Instead of having a NotificationService class directly depend on an EmailService, we can invert the dependency:

class EmailService {
    sendEmail(message) {
        // Send email
    }
}

class NotificationService {
    constructor(emailService) {
        this.emailService = emailService;
    }

    notify(message) {
        this.emailService.sendEmail(message);
    }
}
Enter fullscreen mode Exit fullscreen mode

By doing this, if we decide to change the way we notify users (e.g., using SMS), we can easily swap out the service without changing the NotificationService.


Conclusion

By understanding and applying the SOLID principles, budding software engineers can write more maintainable and scalable code. These principles, while fundamental, are just a starting point. Continual learning and practice, combined with real-world experience, will further refine and solidify one's software design skills.

If you found this article helpful, please share it with your fellow students and developers. Happy coding!


🐦 Follow me on Twitter: @CSTayyab
🔗 Check out my projects on GitHub: @CSTayyab
🌐 Visit my personal website: cstayyab.com

Stay connected and let's build something amazing together! 🚀


Cover Photo by Scott Graham on Unsplash

Top comments (0)