SOLID principles in JavaScript
Krishna Bhamare
Posted on April 18, 2024
In JavaScript, the SOLID principles are a set of guidelines that promote good software design and modular programming.The SOLID principles help in achieving code that is easier to maintain, test, and extend. Here's a brief overview of each principle:
- Single Responsibility Principle (SRP): A class or module should have a single responsibility or reason to change. It states that a class should have only one job or responsibility, and it should encapsulate that responsibility. This principle promotes smaller, focused classes that are easier to understand and maintain.
// Bad example
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
sendEmail(subject, message) {
// Code for sending email
}
saveToDatabase() {
// Code for saving user to the database
}
}
// Good example
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
}
class EmailSender {
sendEmail(user, subject, message) {
// Code for sending email
}
}
class Database {
saveUser(user) {
// Code for saving user to the database
}
}
In the bad example, the User class has multiple responsibilities such as sending emails and saving to the database. The good example separates these responsibilities into separate classes (EmailSender and Database) to adhere to the SRP.
- Open-Closed Principle (OCP): Software entities (classes, modules, functions) should be open for extension but closed for modification. It means that you should be able to add new functionality to a module without modifying its existing code. By using techniques such as inheritance, interfaces, and dependency injection, you can achieve this principle.
// Bad example
class Shape {
constructor(type) {
this.type = type;
}
calculateArea() {
if (this.type === 'circle') {
// Code for calculating circle area
} else if (this.type === 'rectangle') {
// Code for calculating rectangle area
}
}
}
// Good example
class Shape {
calculateArea() {
throw new Error('calculateArea() method should be implemented in derived classes.');
}
}
class Circle extends Shape {
calculateArea() {
// Code for calculating circle area
}
}
class Rectangle extends Shape {
calculateArea() {
// Code for calculating rectangle area
}
}
In the bad example, the Shape class violates the OCP because whenever a new shape type is added, the existing class needs to be modified. The good example uses inheritance and forces the derived classes (Circle and Rectangle) to implement their own calculateArea() method, making it easier to add new shapes without modifying the base class.
- Liskov Substitution Principle (LSP): Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program. This principle emphasizes that subclasses should be able to be used interchangeably with their base classes, without causing errors or unexpected behaviour.
// Bad example
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
calculateArea() {
return this.width * this.height;
}
}
class Square extends Rectangle {
setWidth(width) {
this.width = width;
this.height = width;
}
setHeight(height) {
this.width = height;
this.height = height;
}
}
// Good example
class Shape {
calculateArea() {
throw new Error('calculateArea() method should be implemented in derived classes.');
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
calculateArea() {
return this.width * this.height;
}
}
class Square extends Shape {
constructor(side) {
super();
this.side = side;
}
calculateArea() {
return this.side * this.side;
}
}
In the bad example, the Square class violates the LSP because it doesn't behave as a proper substitute for Rectangle. The good example adheres to the LSP by making Rectangle and Square implement the calculateArea() method independently, without any unexpected side effects.
- Interface Segregation Principle (ISP): Clients should not be forced to depend on interfaces they do not use. This principle suggests that classes should not be forced to depend on interfaces they don't need. Instead, it's better to create smaller and more specific interfaces that are tailored to the requirements of the client.
// Bad example
class Printer {
print(document) {
// Code for printing the document
}
scan(document) {
// Code for scanning the document
}
fax(document) {
// Code for faxing the document
}
}
// Good example
class Printer {
print(document) {
// Code for printing the document
}
}
class Scanner {
scan(document) {
// Code for scanning the document
}
}
class FaxMachine {
fax(document) {
// Code for faxing the document
}
}
In the bad example, the Printer class has unnecessary methods like scan() and fax(). This violates the ISP because clients that only need printing functionality are forced to depend on these additional methods. This can lead to unnecessary coupling and potential issues.
In the good example, we have separate classes for Printer, Scanner, and FaxMachine, each with a single responsibility. This adheres to the ISP because clients can depend on only the interfaces they need. For example, a client requiring scanning functionality can depend on the Scanner class without being burdened by the printing or faxing methods.
By segregating the interfaces into smaller, more focused classes, we achieve better separation of concerns and reduce unnecessary dependencies, leading to more maintainable and flexible code
- Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules. Both should depend on abstractions. This principle encourages the use of abstractions (interfaces or base classes) to define dependencies between modules. It helps in decoupling modules, making them more flexible, and allowing easier substitution of implementations.
These principles work together to promote code that is modular, flexible, and maintainable. By adhering to these principles, you can create JavaScript code that is easier to understand, test, and extend over time.
// Bad example
class UserService {
constructor() {
this.database = new MySQLDatabase();
}
getUser(userId) {
return this.database.getUser(userId);
}
}
class MySQLDatabase {
getUser(userId) {
// Code for retrieving user from MySQL database
}
}
// Good example
class UserService {
constructor(database) {
this.database = database;
}
getUser(userId) {
return this.database.getUser(userId);
}
}
class MongoDBDatabase {
getUser(userId) {
// Code for retrieving user from MongoDB database
}
}
class MySQLDatabase {
getUser(userId) {
// Code for retrieving user from MySQL database
}
}
In the bad example, the UserService directly creates an instance of MySQLDatabase. This creates a tight coupling between UserService and MySQLDatabase, making it difficult to switch to a different type of database (e.g., MongoDB) without modifying the UserService class.
In the good example, the UserService depends on an abstraction (the database parameter), rather than directly creating a database instance. This allows different types of databases (e.g., MongoDBDatabase, MySQLDatabase) to be passed to the UserService at runtime. By depending on an abstraction, the UserService is decoupled from specific database implementations, making it easier to extend and modify the code.
The DIP encourages dependency injection, where dependencies are provided externally rather than being created internally within a class. This promotes flexibility, modularity, and testability, as different implementations can be easily swapped without affecting the high-level module.
Thanks for reading this article..!!!
Posted on April 18, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024