SOLID
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.
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.
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.
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.
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");
}
}
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.