- Last updated: 22 Jan, 2025
The SOLID principles are a set of five design principles that guide object-oriented programming and design. They help developers create systems that are easy to maintain, scale, and extend. These principles are particularly useful for writing clean and maintainable code in Javascript. It was Conceptualized by Robert C. Martin, also known as Uncle Bob. Five Design principles of SOLID principles,
Single Responsibility Principle (SRP):
A class should have only one reason to change, meaning it should perform a single responsibility or task.
Example, Without SRP,
class UserManager {
addUser(user) {
// Add user logic
this.validateUser(user);
this.saveUserToDatabase(user);
this.sendWelcomeEmail(user);
}
validateUser(user) {
// Validation logic
console.log('Validating user:', user);
}
saveUserToDatabase(user) {
// Database logic
console.log('Saving user to the database:', user);
}
sendWelcomeEmail(user) {
// Email logic
console.log('Sending welcome email to:', user.email);
}
}
// Example usage
const userManager = new UserManager();
const user = { name: 'John Doe', email: 'john.doe@example.com' };
userManager.addUser(user);
With SRP,
class UserManager {
constructor(userRepository, emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
addUser(user) {
this.userRepository.save(user);
this.emailService.sendWelcomeEmail(user);
}
}
class UserRepository {
save(user) {
// Save user to database logic
console.log('Saving user to the database:', user);
}
}
class EmailService {
sendWelcomeEmail(user) {
// Email sending logic
console.log('Sending welcome email to:', user.email);
}
}
// Example usage
const userRepository = new UserRepository();
const emailService = new EmailService();
const userManager = new UserManager(userRepository, emailService);
const user = { name: 'John Doe', email: 'john.doe@example.com' };
userManager.addUser(user);
Here, the UserManager, UserValidator, and EmailService each have a single responsibility.
Open/Closed Principle (OCP):
Software entities (classes, modules, functions) should be open for extension but closed for modification.
Without OCP,
class PaymentService {
processPayment(paymentType) {
if (paymentType === "CREDIT_CARD") {
// Credit card processing logic
console.log("Processing credit card payment...");
} else if (paymentType === "PAYPAL") {
// PayPal processing logic
console.log("Processing PayPal payment...");
}
}
}
// Example usage
const paymentService = new PaymentService();
paymentService.processPayment("CREDIT_CARD");
paymentService.processPayment("PAYPAL");
// Define a PaymentProcessor interface (in JavaScript, we use a base class or documentation as a guideline)
class PaymentProcessor {
processPayment() {
throw new Error("processPayment method must be implemented");
}
}
// CreditCardProcessor class
class CreditCardProcessor extends PaymentProcessor {
processPayment() {
// Credit card processing logic
console.log("Processing credit card payment...");
}
}
// PayPalProcessor class
class PayPalProcessor extends PaymentProcessor {
processPayment() {
// PayPal processing logic
console.log("Processing PayPal payment...");
}
}
// PaymentService class
class PaymentService {
constructor(processors) {
this.processors = processors;
}
processPayments() {
this.processors.forEach((processor) => processor.processPayment());
}
}
// Example usage
const processors = [new CreditCardProcessor(), new PayPalProcessor()];
const paymentService = new PaymentService(processors);
paymentService.processPayments();
New payment types can be added without modifying PaymentService.
Liskov Substitution Principle (LSP):
Subtypes must be substitutable for their base types without altering the correctness of the program.
Without LSP,
class Rectangle {
constructor() {
this.width = 0;
this.height = 0;
}
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Rectangle {
setWidth(width) {
super.setWidth(width);
super.setHeight(width); // Violates LSP by overriding base class behavior
}
setHeight(height) {
super.setWidth(height);
super.setHeight(height); // Violates LSP by overriding base class behavior
}
}
// Example usage
const rectangle = new Rectangle();
rectangle.setWidth(4);
rectangle.setHeight(5);
console.log("Rectangle area:", rectangle.getArea()); // 20
const square = new Square();
square.setWidth(4);
console.log("Square area:", square.getArea()); // 16 (Expected behavior if it's a square)
const substitutedRectangle = new Square();
substitutedRectangle.setWidth(4);
substitutedRectangle.setHeight(5);
console.log("Substituted rectangle area:", substitutedRectangle.getArea()); // Incorrect behavior, 25
Problem:The Square class overrides setWidth and setHeight to maintain square-specific behavior, but when used as a Rectangle (substituted for the base type), it violates the LSP because it alters the correctness of the program.
With LSP,
// Shape interface (in JavaScript, we use a base class or documentation for such interfaces)
class Shape {
getArea() {
throw new Error("getArea method must be implemented");
}
}
// Rectangle class implementing Shape
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
// Square class implementing Shape
class Square extends Shape {
constructor(side) {
super();
this.side = side;
}
getArea() {
return this.side * this.side;
}
}
// Example usage
const shapes = [
new Rectangle(4, 5), // Rectangle with width 4 and height 5
new Square(4), // Square with side 4
];
shapes.forEach((shape) => {
console.log("Area:", shape.getArea());
});
Separate Classes: The
Rectangle
andSquare
classes implement theShape
interface independently, each with its own logic forgetArea
.No Dependency on Base Class Behavior:
Square
no longer inherits fromRectangle
, avoiding any incorrect behavior when substituted.Polymorphism: Both
Rectangle
andSquare
are substitutable for theShape
interface without altering correctness.
Here, Square and Rectangle both implement Shape without breaking substitutability.
Interface Segregation Principle (ISP):
Clients should not be forced to implement interfaces they do not use.
Without ISP,
class Vehicle {
startEngine() {
throw new Error("startEngine method must be implemented");
}
fly() {
throw new Error("fly method must be implemented");
}
}
class Car extends Vehicle {
startEngine() {
// Start engine logic
console.log("Car engine started.");
}
fly() {
throw new Error("Cars can't fly"); // Violates ISP
}
}
// Example usage
const car = new Car();
car.startEngine(); // Works fine
car.fly(); // Throws an error: "Cars can't fly"
Problem:The Car class is forced to implement the fly method from the Vehicle interface, even though it doesn't make sense for a car to fly. This violates the Interface Segregation Principle.
With ISP,
// Define the EngineVehicle interface (base class)
class EngineVehicle {
startEngine() {
throw new Error("startEngine method must be implemented");
}
}
// Define the FlyingVehicle interface (base class)
class FlyingVehicle {
fly() {
throw new Error("fly method must be implemented");
}
}
// Car class implements only EngineVehicle
class Car extends EngineVehicle {
startEngine() {
// Start engine logic
console.log("Car engine started.");
}
}
// Airplane class implements both EngineVehicle and FlyingVehicle
class Airplane extends EngineVehicle {
startEngine() {
// Start engine logic
console.log("Airplane engine started.");
}
fly() {
// Fly logic
console.log("Airplane is flying.");
}
}
// Example usage
const car = new Car();
car.startEngine(); // "Car engine started."
const airplane = new Airplane();
airplane.startEngine(); // "Airplane engine started."
airplane.fly(); // "Airplane is flying."
- Separate Interfaces:
EngineVehicle
defines thestartEngine
method.FlyingVehicle
defines thefly
method.
- Class Implementation:
Car
implements onlyEngineVehicle
since it doesn't need flying functionality.Airplane
implements bothEngineVehicle
andFlyingVehicle
since it needs both functionalities.
Adheres to ISP: Classes implement only the methods they need, avoiding unnecessary implementation of unused methods.
Dependency Inversion Principle (DIP):
High-level modules should not depend on low-level modules; both should depend on abstractions.
Without DIP,
class UserRepository {
save(user) {
// Save logic
console.log(`Saving user: ${user.name}`);
}
}
class UserService {
constructor() {
this.userRepository = new UserRepository(); // High-level module directly depends on a low-level module
}
saveUser(user) {
this.userRepository.save(user);
}
}
// Example usage
const userService = new UserService();
userService.saveUser({ name: "John Doe" });
Problem:The UserService class (high-level module) depends directly on the UserRepository class (low-level module), creating tight coupling between the two.
With DIP,
// Define the UserRepository interface
class UserRepository {
save(user) {
throw new Error("save method must be implemented");
}
}
// Implement the JpaUserRepository class (low-level module)
class JpaUserRepository extends UserRepository {
save(user) {
// Save logic
console.log(`Saving user: ${user.name}`);
}
}
// UserService depends on the abstraction (UserRepository), not the concrete implementation
class UserService {
constructor(userRepository) {
this.userRepository = userRepository; // Dependency injected
}
saveUser(user) {
this.userRepository.save(user);
}
}
// Example usage
const userRepository = new JpaUserRepository(); // Low-level module
const userService = new UserService(userRepository); // High-level module depends on abstraction
userService.saveUser({ name: "John Doe" }); // Output: Saving user: John Doe
By depending on UserRepository, the UserService can work with any repository implementation.
Abstraction:The
UserRepository
class acts as an interface that declares thesave
method.Low-Level Module: The
JpaUserRepository
class extendsUserRepository
and provides a concrete implementation of thesave
method.High-Level Module: The
UserService
class depends on theUserRepository
abstraction, not a concrete implementation.Dependency Injection: The
UserRepository
dependency is passed intoUserService
via its constructor, decoupling the two and allowing flexibility to swap out implementations.
By following these SOLID principles, you ensure your Java code is modular, reusable, and easier to maintain.
Similar Post
SOLID Principles Using Javascript
- 22 Jan, 2025
- 3 min read
Master the SOLID principles in JavaScript to create robust, maintainable, and scalable software. Ensure clean code through single responsibility, extensibility, substitutability, focused interfaces, and effective dependency management.
SOLID Principles Using PHP
- 22 Jan, 2025
- 3 min read
Master SOLID principles to build resilient, scalable software with maintainable, extensible, and decoupled code that is easier to test and evolve.