If you want to write cleaner, more maintainable, and scalable software, mastering the SOLID principles is essential. These five core object-oriented design principles empower you to build systems that are easier to understand, extend, and test — without introducing chaos.
Here's a quick breakdown:
S — Single Responsibility Principle (SRP)
Every class or module should have one clear responsibility.
When a class tries to do too many things, it becomes harder to maintain and more prone to bugs.
O — Open/Closed Principle (OCP)
Software should be open for extension, but closed for modification.
You should be able to add new features without rewriting existing code, minimizing the risk of regressions.
L — Liskov Substitution Principle (LSP)
Subtypes must be replaceable for their base types without altering the program’s behavior.
If a subclass breaks the expectations set by the parent class, it violates this principle.
I — Interface Segregation Principle (ISP)
Don't force a class to implement methods it doesn't need.
Instead of one large interface, create smaller, role-specific interfaces.
D — Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules — both should depend on abstractions.
This decouples your code and makes it easier to modify and test.
The SOLID principles are foundational guidelines for object-oriented design. They help you create clean, maintainable, and scalable software systems. This guide includes detailed explanations and practical TypeScript examples to illustrate each principle.
S — Single Responsibility Principle (SRP)
Definition: A class should have only one reason to change, meaning it should have a single, well-defined responsibility.
Why It Matters: Mixing responsibilities (e.g., validation and persistence) makes a class fragile—changes to one task can break others. SRP improves modularity and testability.
Violation Example:
// SRP Violating Example
interface Order {
id: number;
items: string[];
total: number;
customerEmail: string;
}
class OrderProcessor {
processOrder(order: Order): void {
if (!order) throw new Error("Order cannot be null");
// 1. Validate order
if (order.items.length === 0) {
throw new Error("Order must have at least one item");
}
// 2. Process payment
console.log(`Processing payment of $${order.total} for order ${order.id}`);
// 3. Save to database
console.log(`Saving order ${order.id} with ${order.items.length} items to database`);
// 4. Send confirmation email
console.log(`Sending confirmation email to ${order.customerEmail}`);
}
}
This class handles too much, making it hard to maintain.
Refactored (SRP Applied):
interface Order {
id: number;
items: string[];
total: number;
}
class OrderValidator {
validate(order: Order): void {
if (!order) throw new Error("Order cannot be null");
}
}
class PaymentProcessor {
processPayment(order: Order): void {
console.log(`Processing payment for order ${order.id}`);
}
}
class OrderRepository {
save(order: Order): void {
console.log(`Saving order ${order.id} to database`);
}
}
class EmailNotifier {
sendConfirmation(order: Order): void {
console.log(`Sending confirmation email for order ${order.id}`);
}
}
class OrderProcessor {
private validator: OrderValidator;
private paymentProcessor: PaymentProcessor;
private repository: OrderRepository;
private notifier: EmailNotifier;
constructor(
validator: OrderValidator,
paymentProcessor: PaymentProcessor,
repository: OrderRepository,
notifier: EmailNotifier
) {
this.validator = validator;
this.paymentProcessor = paymentProcessor;
this.repository = repository;
this.notifier = notifier;
}
process(order: Order): void {
this.validator.validate(order);
this.paymentProcessor.processPayment(order);
this.repository.save(order);
this.notifier.sendConfirmation(order);
}
}
Each class now focuses on one task, enhancing clarity and flexibility.
O — Open/Closed Principle (OCP)
Definition: Software entities should be open for extension but closed for modification.
Why It Matters: Modifying existing code risks bugs. OCP allows new functionality via extensions (e.g., interfaces) without altering tested code.
Violation Example:
class InvoicePrinter {
print(invoice: any, format: string): void {
if (format === "PDF") {
// Generate PDF
} else if (format === "Excel") {
// Generate Excel
}
}
}
Adding a new format requires changing this class.
Refactored (OCP Applied):
interface Invoice {
id: number;
amount: number;
}
interface IInvoicePrinter {
print(invoice: Invoice): void;
}
class PdfInvoicePrinter implements IInvoicePrinter {
print(invoice: Invoice): void {
console.log(`Printing PDF invoice for invoice ${invoice.id}`);
}
}
class ExcelInvoicePrinter implements IInvoicePrinter {
print(invoice: Invoice): void {
console.log(`Printing Excel invoice for invoice ${invoice.id}`);
}
}
class InvoiceService {
private printer: IInvoicePrinter;
constructor(printer: IInvoicePrinter) {
if (!printer) throw new Error("Printer cannot be null");
this.printer = printer;
}
printInvoice(invoice: Invoice): void {
this.printer.print(invoice);
}
}
New printers (e.g., HtmlInvoicePrinter
) can be added without modifying InvoiceService
.
L — Liskov Substitution Principle (LSP)
Definition: Subtypes must be substitutable for their base types without altering expected behavior.
Why It Matters: Violating LSP causes errors when subclasses don’t meet the parent’s contract.
Violation Example:
class Vehicle {
startEngine(): void {
// Start engine
}
}
class ElectricCar extends Vehicle {
startEngine(): void {
throw new Error("Electric cars don’t have engines!");
}
}
ElectricCar
breaks code expecting startEngine
.
Refactored (LSP Respected):
abstract class Vehicle {
abstract start(): void;
}
interface ICombustionVehicle {
startEngine(): void;
}
class GasolineCar extends Vehicle implements ICombustionVehicle {
start(): void {
this.startEngine();
}
startEngine(): void {
console.log("Gasoline engine started.");
}
}
class ElectricCar extends Vehicle {
start(): void {
console.log("Electric motor activated.");
}
}
Both subclasses work as Vehicle
without issues.
I — Interface Segregation Principle (ISP)
Definition: Classes shouldn’t be forced to implement unused interfaces.
Why It Matters: Large interfaces create unnecessary dependencies. ISP favors smaller, specific interfaces.
Violation Example:
interface IWorker {
work(): void;
takeBreak(): void;
}
class Robot implements IWorker {
work(): void {
console.log("Robot working...");
}
takeBreak(): void {
throw new Error("Robots don’t take breaks!");
}
}
Robot
is forced to implement an irrelevant method.
Refactored (ISP Applied):
interface IWorkable {
work(): void;
}
interface IBreakable {
takeBreak(): void;
}
class Human implements IWorkable, IBreakable {
work(): void {
console.log("Human working...");
}
takeBreak(): void {
console.log("Human taking a break...");
}
}
class Robot implements IWorkable {
work(): void {
console.log("Robot working...");
}
}
Robot
only implements what it needs.
D — Dependency Inversion Principle (DIP)
Definition: High-level modules should depend on abstractions, not low-level details.
Why It Matters: Tight coupling limits flexibility. DIP uses abstractions for loose coupling.
Violation Example:
class Notification {
private emailService = new EmailService();
send(message: string): void {
this.emailService.sendEmail(message);
}
}
class EmailService {
sendEmail(message: string): void {
console.log(`Email sent: ${message}`);
}
}
Notification
is tied to EmailService
.
Refactored (DIP Applied):
interface IMessageService {
send(message: string): void;
}
class EmailService implements IMessageService {
send(message: string): void {
console.log(`Email sent: ${message}`);
}
}
class SmsService implements IMessageService {
send(message: string): void {
console.log(`SMS sent: ${message}`);
}
}
class Notification {
private service: IMessageService;
constructor(service: IMessageService) {
if (!service) throw new Error("Service cannot be null");
this.service = service;
}
send(message: string): void {
this.service.send(message);
}
}
Notification
works with any IMessageService
implementation.
Final Thoughts
Applying SOLID in TypeScript yields cleaner, more testable, and adaptable code.
Top comments (0)