Understanding the SOLID Principles in JavaScript
Muhammad Tayyab Sheikh
Posted on October 11, 2023
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}`);
}
}
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
}
}
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();
}
}
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;
}
}
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;
}
}
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!
}
}
A better approach:
class Bird {
fly() {
// Implementation
}
}
class SwimmingBird extends Bird {
swim() {
// Now only birds that can swim will have this method
}
}
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);
}
}
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
Posted on October 11, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024