The SOLID principles are design principles in object-oriented programming that help developers create more understandable, flexible, and maintainable software.
Let's dive into each principle and see how they can be applied using JavaScript.
📌 1. Single Responsibility Principle (SRP)
Definition: A class should have only one reason to change, meaning it should have only one job or responsibility.
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
}
class UserService {
createUser(user) {
// logic to create user
}
getUser(id) {
// logic to get user
}
}
class UserNotificationService {
sendWelcomeEmail(user) {
// logic to send email
}
}
const user = new User('John Doe', 'john.doe@example.com');
const userService = new UserService();
userService.createUser(user);
const notificationService = new UserNotificationService();
notificationService.sendWelcomeEmail(user);
Here, User
handles the user data, UserService
handles user-related operations, and UserNotificationService
handles notifications. Each class has a single responsibility.
📌 2. Open/Closed Principle (OCP)
Definition: Software entities should be open for extension but closed for modification.
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
area() {
return this.width * this.height;
}
}
class Circle {
constructor(radius) {
this.radius = radius;
}
area() {
return Math.PI * Math.pow(this.radius, 2);
}
}
const shapes = [new Rectangle(4, 5), new Circle(3)];
const totalArea = shapes.reduce((sum, shape) => sum + shape.area(), 0);
console.log(totalArea);
In this example, each shape class's area
method (like Rectangle
and Circle
) can be extended without modifying the existing code of the shape classes. This allows for adding new shapes in the future without changing the existing ones.
📌 3. Liskov Substitution Principle (LSP)
Definition: Subtypes must be substitutable for their base types without altering the correctness of the program.
class Bird {
fly() {
console.log('I can fly');
}
}
class Duck extends Bird {}
class Ostrich extends Bird {
fly() {
throw new Error('I cannot fly');
}
}
function makeBirdFly(bird) {
bird.fly();
}
const duck = new Duck();
makeBirdFly(duck); // Works fine
const ostrich = new Ostrich();
makeBirdFly(ostrich); // Throws error
In this example, Ostrich
violates LSP because it changes the expected behavior of the fly
method. To comply with LSP, we should ensure that subclasses do not change the behavior expected by the base class.
📌 4. Interface Segregation Principle (ISP)
Definition: Clients should not be forced to depend on interfaces they do not use.
class Printer {
print() {
console.log('Printing document');
}
}
class Scanner {
scan() {
console.log('Scanning document');
}
}
class MultiFunctionPrinter {
print() {
console.log('Printing document');
}
scan() {
console.log('Scanning document');
}
}
const printer = new Printer();
printer.print();
const scanner = new Scanner();
scanner.scan();
const multiFunctionPrinter = new MultiFunctionPrinter();
multiFunctionPrinter.print();
multiFunctionPrinter.scan();
Here, Printer
and Scanner
classes provide specific functionalities without forcing clients to implement methods they don't need. The MultiFunctionPrinter
can use both functionalities, adhering to the ISP.
📌 5. Dependency Inversion Principle (DIP)
Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.
class NotificationService {
constructor(sender) {
this.sender = sender;
}
sendNotification(message) {
this.sender.send(message);
}
}
class EmailSender {
send(message) {
console.log(`Sending email: ${message}`);
}
}
class SMSSender {
send(message) {
console.log(`Sending SMS: ${message}`);
}
}
const emailSender = new EmailSender();
const notificationService = new NotificationService(emailSender);
notificationService.sendNotification('Hello via Email');
const smsSender = new SMSSender();
const notificationServiceWithSMS = new NotificationService(smsSender);
notificationServiceWithSMS.sendNotification('Hello via SMS');
In this example, the NotificationService
depends on an abstraction (sender
), allowing it to work with any sender implementation (like EmailSender
or SMSSender
). This adheres to DIP by making the high-level module (NotificationService
) depend on abstractions rather than concrete implementations.
Conclusion ✅
By adhering to the SOLID principles, you can design JavaScript applications that are more robust, maintainable, and scalable.
These principles help to ensure that your codebase remains clean and flexible, making it easier to manage and extend as your application grows.
Applying these principles consistently can significantly improve the quality of your software.
Happy Coding! 🔥
LinkedIn, X (Twitter), Telegram, YouTube, Discord, Facebook, Instagram
Top comments (9)
DIP -- The example is okay, but it is not clear what "inversion" means :)
Inversion means that the abstraction now belongs to the client code, not to the server code. This means that the NotificationService defines which method of the Sender to call, not the EmailSender or SMSSender. This contrasts with a case where the NotificationService explicitly uses one of the implementations:
Your example shows how to achieve this inversion with dependency injection. There are other ways to do this, such as using a service locator, but only in cases where the use of a service locator is appropriate.
Clean code and clear information
@alisamirali
Unfortunately, it is not a rule that throwing an exception violates the LSP. TypeScript and JavaScript don't have the capability to express whether exception throwing is expected behavior or not. In some cases, it will be expected; in others, it will not.
Let's suppose we are adding an exception for the Duck class in case the duck is "shot by a hunter." This means that the Bird class must assume that an exception can be thrown.
The Liskov Substitution Principle (LSP) is one of the most difficult principles to understand and explain.
A good example is as follows:
Now, to make the plane fly, we need to fuel it first by calling the method
plane.fuel(100)
, whereas for birds, this is not needed.ISP -- miss. Totaly.
ISP for Javascript doesn't have any sence ) We don't have a way to declare the dependency of a function or a class.
For TS, it makes sense to look on the
UserService
example taken from you SRP:Here we have
UserServiceImpl
andNotificationServiceImpl
dependent on the different interface implemented by the same class.First of all it allows us to write unit tests with more simple mocking and what is more important we can provide different implementation to this classes.
For example we can provide original
users: UserRepoImpl
to theUserService
and we can patt the object with methodfindById
that decorates the original one with some caching logic, meaning we will apply Decorator Pattern but only forUserByIdProvider
interface, not forUserRepo
entier one.And yes, for JS -- it has no sense, because we don't have any declaration
@alisamirali
Can you explain your point:
How does the apportunity to extend Rectangle or Circle help us to add a new figure class?
And actually I'd like to ask, which code in your example is open and which one is closed for modification?
Thank you
Extremely useful!
I didn't know about SOLID.
I only knew some few generalized characteristics of classes but your sharing have gave me more specific understanding; I was wondering if you can share any other resources to these topics?
Passing a user to create a user seems wild 😜
Would it be bad to have MultiFunctionPrinter extend Printer and Scanner?
Some comments may only be visible to logged-in visitors. Sign in to view all comments. Some comments have been hidden by the post's author - find out more