🛠️ Cracking the Code: Master SOLID Principles in JavaScript with Real-World Examples 🚀
Hamza Khan
Posted on October 19, 2024
When writing clean, maintainable code, developers often look to SOLID principles as a guiding framework. These five principles help make code more readable, reusable, and easier to manage. In this post, we will break down each principle, from the basics to more advanced examples, to help you master SOLID principles in JavaScript.
What Are the SOLID Principles?
SOLID is an acronym representing five core principles of object-oriented programming, and they apply equally well in JavaScript, especially in class-based and object-oriented systems. Here's a quick breakdown:
- S - Single Responsibility Principle (SRP)
- O - Open/Closed Principle (OCP)
- L - Liskov Substitution Principle (LSP)
- I - Interface Segregation Principle (ISP)
- D - Dependency Inversion Principle (DIP)
Let’s dive deeper into each one and look at how we can apply these principles using JavaScript.
1. 🧩 Single Responsibility Principle (SRP)
Definition: A class should have only one reason to change. It should have a single responsibility or job.
A common mistake is to have "God classes" that try to do everything. For example, a User
class should only handle user-specific tasks, not things like email notifications.
Bad Example:
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
sendWelcomeEmail() {
// Sending email
console.log(`Sending welcome email to ${this.email}`);
}
saveToDatabase() {
// Save to database
console.log(`${this.name} saved to the database.`);
}
}
Good Example:
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
}
class UserService {
saveToDatabase(user) {
console.log(`${user.name} saved to the database.`);
}
}
class EmailService {
sendWelcomeEmail(user) {
console.log(`Sending welcome email to ${user.email}`);
}
}
// Single responsibility for each service
const user = new User("Alice", "alice@example.com");
const userService = new UserService();
const emailService = new EmailService();
userService.saveToDatabase(user);
emailService.sendWelcomeEmail(user);
By separating concerns, each class now handles just one job.
2. 🏗️ Open/Closed Principle (OCP)
Definition: Software entities should be open for extension but closed for modification.
You should be able to add new functionality to a class without altering its existing code. This principle encourages the use of inheritance or interfaces.
Bad Example:
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
area() {
return this.width * this.height;
}
}
class AreaCalculator {
calculate(shape) {
if (shape instanceof Rectangle) {
return shape.area();
}
// More conditions for other shapes...
}
}
Good Example (following OCP):
class Shape {
area() {
throw new Error("This method should be overridden!");
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
area() {
return this.width * this.height;
}
}
class Circle extends Shape {
constructor(radius) {
super();
this.radius = radius;
}
area() {
return Math.PI * this.radius * this.radius;
}
}
class AreaCalculator {
calculate(shape) {
return shape.area();
}
}
// No need to modify existing code when adding new shapes
const rectangle = new Rectangle(10, 20);
const circle = new Circle(5);
const calculator = new AreaCalculator();
console.log(calculator.calculate(rectangle)); // 200
console.log(calculator.calculate(circle)); // 78.54
By making Shape
an abstract class, we can extend new shapes without modifying existing code.
3. 🔄 Liskov Substitution Principle (LSP)
Definition: Subtypes must be substitutable for their base types without altering the correctness of the program.
This means that you should be able to replace a parent class with its subclass without breaking the program. If a subclass can’t fulfill all promises of the parent class, it violates this principle.
Bad Example:
class Bird {
fly() {
console.log("Flying");
}
}
class Penguin extends Bird {
fly() {
throw new Error("Penguins can't fly!");
}
}
Good Example (using LSP):
class Bird {
move() {
console.log("Moving");
}
}
class FlyingBird extends Bird {
fly() {
console.log("Flying");
}
}
class Penguin extends Bird {
swim() {
console.log("Swimming");
}
}
const eagle = new FlyingBird();
const penguin = new Penguin();
eagle.fly(); // Works as expected
penguin.move(); // Penguins can still move without the expectation to fly
By creating FlyingBird
and Penguin
separately, we respect LSP and avoid introducing issues.
4. đź“„ Interface Segregation Principle (ISP)
Definition: No client should be forced to depend on methods it does not use.
In JavaScript, while we don’t have traditional interfaces, we can still follow this principle by splitting large interfaces into more specific ones.
Bad Example:
class Animal {
eat() {}
fly() {}
swim() {}
}
class Dog extends Animal {
fly() {
throw new Error("Dogs can't fly");
}
swim() {
console.log("Dogs can swim");
}
}
Good Example:
class Eater {
eat() {
console.log("Eating");
}
}
class Swimmer {
swim() {
console.log("Swimming");
}
}
class Flyer {
fly() {
console.log("Flying");
}
}
class Dog extends Eater {}
class Duck extends Eater {
swim() {
console.log("Swimming");
}
fly() {
console.log("Flying");
}
}
const dog = new Dog();
const duck = new Duck();
dog.eat(); // Works
duck.fly(); // Works
duck.swim();// Works
By creating separate, specific classes (Eater
, Swimmer
, Flyer
), we don’t force classes like Dog
to implement unused methods.
5. 🔧 Dependency Inversion Principle (DIP)
Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions.
You should depend on abstractions (interfaces) rather than on concrete implementations.
Bad Example:
class Database {
connect() {
console.log("Connecting to the database...");
}
}
class UserService {
constructor() {
this.db = new Database();
}
saveUser(user) {
this.db.connect();
console.log(`Saving user ${user}`);
}
}
Good Example (following DIP):
class Database {
connect() {
throw new Error("This method should be overridden!");
}
}
class MySQLDatabase extends Database {
connect() {
console.log("Connecting to MySQL...");
}
}
class UserService {
constructor(database) {
this.db = database;
}
saveUser(user) {
this.db.connect();
console.log(`Saving user ${user}`);
}
}
const db = new MySQLDatabase();
const userService = new UserService(db);
userService.saveUser("Alice");
By passing the Database
dependency via the constructor, we decouple UserService
from a specific database implementation.
🔥 Conclusion
Understanding and applying SOLID principles is essential to writing maintainable, scalable, and testable code. Whether you're working on a small project or a large-scale application, adhering to these principles in JavaScript ensures your code remains flexible, reduces bugs, and makes future development easier.
Are you applying SOLID principles in your current projects? If not, start with small steps—refactor classes with single responsibilities or abstract dependencies—and gradually make these principles part of your development workflow.
🧠Key Takeaways:
- SRP: Keep each class focused on one task.
- OCP: Extend behavior without altering existing code.
- LSP: Ensure subclasses can replace their parent without issues.
- ISP: Don’t force classes to implement methods they don’t need.
- DIP: Depend on abstractions, not concrete implementations.
Posted on October 19, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
October 19, 2024