SOLID Principles in Programming
SOLID is a set of programming principles developed by Robert Martin (Uncle Bob). These principles help create clean, scalable, and maintainable code.
Single Responsibility Principle (SRP) — The Single Responsibility Principle
Each class should have only one reason to change. In other words, it should be responsible for only one functionality.
Bad Example:
class Report {
generateReport() {
// logic for generating a report
}
printReport() {
// logic for printing
}
saveToFile() {
// logic for saving to a file
}
}
In this code, the Report class handles report generation, printing, and saving simultaneously, which violates SRP.
Good Example:
class ReportGenerator {
generate() {
// logic for generating a report
}
}
class ReportPrinter {
print() {
// logic for printing
}
}
class ReportSaver {
saveToFile() {
// logic for saving to a file
}
}
Here, each class is responsible for only one action, making the code more readable and manageable.
Open/Closed Principle (OCP) — The Open/Closed Principle
Software code should be closed for modification but open for extension.
Bad Example:
class Payment {
process(type: string) {
if (type === "credit") {
// credit card payment logic
} else if (type === "paypal") {
// PayPal payment logic
} else if (type === "crypto") {
// crypto payment logic
}
}
}
The problem here is that if we want to add a new payment method, we are forced to modify the Payment class.
Good Example:
interface PaymentMethod {
process(): void;
}
class CreditCardPayment implements PaymentMethod {
process() {
console.log("Processing credit card payment");
}
}
class PayPalPayment implements PaymentMethod {
process() {
console.log("Processing PayPal payment");
}
}
class CryptoPayment implements PaymentMethod {
process() {
console.log("Processing crypto payment");
}
}
class PaymentProcessor {
constructor(private paymentMethod: PaymentMethod) {}
processPayment() {
this.paymentMethod.process();
}
}
Here, if we want to add a new payment method, we simply create a new class implementing the PaymentMethod interface without modifying the existing code.
Liskov Substitution Principle (LSP) — The Liskov Substitution Principle
Subclasses should be able to replace their parent class without the need for modification.
Bad Example:
class Bird {
fly() {
console.log("Flying");
}
}
class Penguin extends Bird {
fly() {
throw new Error("Penguins cannot fly");
}
}
In this example, the Penguin class violates LSP because it cannot behave like its parent Bird class.
Good Example:
interface Bird {
move(): void;
}
class FlyingBird implements Bird {
move() {
console.log("Flying");
}
}
class NonFlyingBird implements Bird {
move() {
console.log("Walking");
}
}
class Penguin extends NonFlyingBird {}
Here, we have created FlyingBird and NonFlyingBird, allowing us to correctly classify flying and non-flying birds.
Interface Segregation Principle (ISP) — The Interface Segregation Principle
Do not force classes to implement methods they do not need.
Bad Example:
interface Worker {
work(): void;
eat(): void;
}
class Robot implements Worker {
work() {
console.log("Working");
}
eat() {
throw new Error("Robots do not eat");
}
}
Good Example:
interface Workable {
work(): void;
}
interface Eatable {
eat(): void;
}
class Human implements Workable, Eatable {
work() {
console.log("Working");
}
eat() {
console.log("Eating");
}
}
class Robot implements Workable {
work() {
console.log("Working");
}
}
Dependency Inversion Principle (DIP) — The Dependency Inversion Principle
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Bad Example:
class Database {
save(data: string) {
console.log("Saving data to MySQL");
}
}
class UserService {
private db = new Database();
saveUser(user: string) {
this.db.save(user);
}
}
Good Example:
interface Database {
save(data: string): void;
}
class MySQLDatabase implements Database {
save(data: string) {
console.log("Saving data to MySQL");
}
}
class MongoDBDatabase implements Database {
save(data: string) {
console.log("Saving data to MongoDB");
}
}
class UserService {
constructor(private db: Database) {}
saveUser(user: string) {
this.db.save(user);
}
}
These principles will help you write cleaner, more flexible, and scalable code while avoiding architectural issues.