S - Single
Responibility Principle
O - Open/Closed
Principle
L - Liskov
Substitution Principle
I - Interface
Segmented Principle
D - Dependency
Inversion Principle
Advantages of following the solid principles,
Help us to write better code -
1. avoid duplicate code
2. easy to maintain code
3. easy to understand
4. flexible software
5. reduce complexity
A class should have only one Responibility and one reason to change.
// Without adhering to SRP
class Report {
constructor(title, content) {
this.title = title;
this.content = content;
}
generateReport() {
console.log(`Generating report for ${this.title}:\n${this.content}`);
}
saveToDatabase() {
console.log(`Saving report to the database: ${this.title}`);
}
}
// With SRP
class Report {
constructor(title, content) {
this.title = title;
this.content = content;
}
generateReport() {
console.log(`Generating report for ${this.title}:\n${this.content}`);
}
}
class ReportSaver {
saveToDatabase(report) {
console.log(`Saving report to the database: ${report.title}`);
}
}
// Usage
const report = new Report("Monthly Report", "This is the content of the report.");
report.generateReport();
const reportSaver = new ReportSaver();
reportSaver.saveToDatabase(report);
Open for extension but Closed for modification
// Without adhering to OCP
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
calculateArea() {
return this.width * this.height;
}
}
// Now, imagine there's a requirement to add support for calculating the perimeter.
// Without OCP, you might be tempted to modify the existing class:
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
calculateArea() {
return this.width * this.height;
}
calculatePerimeter() {
return 2 * (this.width + this.height);
}
}
// With OCP
// Instead, you can create an interface or abstract class
class Shape {
calculateArea() {
throw new Error("Method not implemented");
}
}
// Extend the Shape class with the specific implementation (Rectangle)
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
calculateArea() {
return this.width * this.height;
}
}
// Now, you can easily add new shapes without modifying existing code
class Circle extends Shape {
constructor(radius) {
super();
this.radius = radius;
}
calculateArea() {
return Math.PI * Math.pow(this.radius, 2);
}
}
If class B is a subtype of class A, then we should be able to replace object of A with B without breaking the behavior of the program
// Without adhering to LSP
class Bird {
fly() {
console.log("Flying...");
}
}
class Penguin extends Bird {
// Penguins cannot fly, but we are violating LSP by not providing a fly method
}
// Usage
function makeBirdFly(bird) {
bird.fly();
}
const bird = new Bird();
const penguin = new Penguin();
makeBirdFly(bird); // Outputs: Flying...
makeBirdFly(penguin); // Error or unexpected behavior because penguins can't fly
// Adhering to LSP
class Bird {
fly() {
console.log("Flying...");
}
}
class Penguin extends Bird {
fly() {
console.log("Penguins can't fly!");
}
}
// Usage
function makeBirdFly(bird) {
bird.fly();
}
const bird = new Bird();
const penguin = new Penguin();
makeBirdFly(bird); // Outputs: Flying...
makeBirdFly(penguin); // Outputs: Penguins can't fly!
Interfaces should be such, that client should not implement unnecessary functions they do not need.
// Without adhering to ISP
// A monolithic interface with methods for both printing and scanning
class MultiFunctionDevice {
print() {
console.log("Printing...");
}
scan() {
console.log("Scanning...");
}
}
// Classes that implement the monolithic interface
class Printer extends MultiFunctionDevice {}
class Scanner extends MultiFunctionDevice {}
// Usage
const printer = new Printer();
const scanner = new Scanner();
printer.print(); // Outputs: Printing...
printer.scan(); // Outputs: Scanning...
scanner.print(); // Outputs: Printing... (Unexpected behavior for a scanner)
scanner.scan(); // Outputs: Scanning...
// Adhering to ISP
// Separate interfaces for printing and scanning
class Printer {
print() {
console.log("Printing...");
}
}
class Scanner {
scan() {
console.log("Scanning...");
}
}
// Usage
const printer = new Printer();
const scanner = new Scanner();
printer.print(); // Outputs: Printing...
scanner.scan(); // Outputs: Scanning...
// scanner.print(); // Error - 'print' is not a function for a Scanner
Class should depend on interface rather than concrete classes.
// Without DIP
class Switch {
turnOn() {
console.log("Device turned on");
}
turnOff() {
console.log("Device turned off");
}
}
class Fan extends Switch {
// Fan-specific functionality
}
// Usage
const fanSwitch = new Fan();
fanSwitch.turnOn(); // Outputs: Device turned on
fanSwitch.turnOff(); // Outputs: Device turned off
-------
// Adhering to DIP
// Abstraction
class Switchable {
turnOn() {
throw new Error("Method not implemented");
}
turnOff() {
throw new Error("Method not implemented");
}
}
// Low-level module
class Switch extends Switchable {
turnOn() {
console.log("Device turned on");
}
turnOff() {
console.log("Device turned off");
}
}
// High-level module
class Fan {
constructor(device) {
this.device = device;
}
operate() {
this.device.turnOn();
// Additional fan-specific logic here
this.device.turnOff();
}
}
// Usage
const switchableDevice = new Switch();
const fan = new Fan(switchableDevice);
fan.operate(); // Outputs: Device turned on, Device turned off