Open/Closed Principle (OCP)
"Software entities should be open for extension but closed for modification."
The Open/Closed Principle means you should be able to add new functionality without changing existing code. This prevents introducing bugs in tested code when adding features.
The Problem: Violating OCP
// ❌ BAD: Must modify code to add new payment types
class PaymentProcessor {
processPayment(amount: number, type: string): void {
if (type === 'credit') {
console.log(`Processing credit card payment: $${amount}`);
// Credit card processing logic
const fee = amount * 0.029;
console.log(`Fee: $${fee}`);
} else if (type === 'paypal') {
console.log(`Processing PayPal payment: $${amount}`);
// PayPal processing logic
const fee = amount * 0.034;
console.log(`Fee: $${fee}`);
} else if (type === 'bitcoin') {
console.log(`Processing Bitcoin payment: $${amount}`);
// Bitcoin processing logic
const fee = 0.5; // Flat fee
console.log(`Fee: $${fee}`);
}
// To add a new payment type, we must modify this class!
// Risk of breaking existing functionality
}
}
// Problems:
// 1. Every new payment type requires modifying this class
// 2. Risk of introducing bugs in existing payment methods
// 3. Violates single responsibility (handles all payment types)
// 4. Hard to test each payment type in isolationThe Solution: Applying OCP
// ✅ GOOD: Open for extension, closed for modification
// Define interface (closed for modification)
interface PaymentMethod {
processPayment(amount: number): void;
calculateFee(amount: number): number;
}
// Concrete implementations (open for extension)
class CreditCardPayment implements PaymentMethod {
processPayment(amount: number): void {
console.log(`Processing credit card payment: $${amount}`);
const fee = this.calculateFee(amount);
console.log(`Fee: $${fee}`);
// Credit card specific logic
}
calculateFee(amount: number): number {
return amount * 0.029 + 0.30; // 2.9% + $0.30
}
}
class PayPalPayment implements PaymentMethod {
processPayment(amount: number): void {
console.log(`Processing PayPal payment: $${amount}`);
const fee = this.calculateFee(amount);
console.log(`Fee: $${fee}`);
// PayPal specific logic
}
calculateFee(amount: number): number {
return amount * 0.034 + 0.49; // 3.4% + $0.49
}
}
class BitcoinPayment implements PaymentMethod {
processPayment(amount: number): void {
console.log(`Processing Bitcoin payment: $${amount}`);
const fee = this.calculateFee(amount);
console.log(`Fee: $${fee}`);
// Bitcoin specific logic
}
calculateFee(amount: number): number {
return 0.5; // Flat fee
}
}
// Easy to add new payment types without modifying existing code!
class ApplePayPayment implements PaymentMethod {
processPayment(amount: number): void {
console.log(`Processing Apple Pay: $${amount}`);
const fee = this.calculateFee(amount);
console.log(`Fee: $${fee}`);
}
calculateFee(amount: number): number {
return amount * 0.015; // 1.5%
}
}
// Payment processor is now closed for modification
class PaymentProcessor {
processPayment(amount: number, method: PaymentMethod): void {
method.processPayment(amount);
}
}
// Usage
const processor = new PaymentProcessor();
processor.processPayment(100, new CreditCardPayment());
processor.processPayment(100, new PayPalPayment());
processor.processPayment(100, new BitcoinPayment());
processor.processPayment(100, new ApplePayPayment()); // New type, no changes to processor!Real-World Example: Reporting System
// ❌ BAD: Must modify code for each new report format
class ReportGenerator {
generate(data: any[], format: string): string {
if (format === 'pdf') {
return this.generatePDF(data);
} else if (format === 'excel') {
return this.generateExcel(data);
} else if (format === 'html') {
return this.generateHTML(data);
}
return '';
}
private generatePDF(data: any[]): string {
// PDF generation logic
return 'PDF content';
}
private generateExcel(data: any[]): string {
// Excel generation logic
return 'Excel content';
}
private generateHTML(data: any[]): string {
// HTML generation logic
return 'HTML content';
}
}
// ✅ GOOD: Open for extension
interface ReportFormatter {
format(data: any[]): string;
getFileExtension(): string;
}
class PDFFormatter implements ReportFormatter {
format(data: any[]): string {
// PDF-specific formatting
return `PDF Report with ${data.length} records`;
}
getFileExtension(): string {
return '.pdf';
}
}
class ExcelFormatter implements ReportFormatter {
format(data: any[]): string {
// Excel-specific formatting
return `Excel Report with ${data.length} records`;
}
getFileExtension(): string {
return '.xlsx';
}
}
class HTMLFormatter implements ReportFormatter {
format(data: any[]): string {
return `<html>
<body>
<h1>Report</h1>
<p>Records: ${data.length}</p>
</body>
</html>`;
}
getFileExtension(): string {
return '.html';
}
}
// Adding new format is easy - no changes to existing code!
class CSVFormatter implements ReportFormatter {
format(data: any[]): string {
if (data.length === 0) return '';
const headers = Object.keys(data[0]).join(',');
const rows = data.map(row =>
Object.values(row).join(',')
).join('\n');
return `${headers}\n${rows}`;
}
getFileExtension(): string {
return '.csv';
}
}
class JSONFormatter implements ReportFormatter {
format(data: any[]): string {
return JSON.stringify(data, null, 2);
}
getFileExtension(): string {
return '.json';
}
}
// Report generator is closed for modification
class ReportGenerator {
generate(data: any[], formatter: ReportFormatter): { content: string; extension: string } {
return {
content: formatter.format(data),
extension: formatter.getFileExtension()
};
}
}
// Usage
const data = [
{ id: 1, name: 'Alice', sales: 10000 },
{ id: 2, name: 'Bob', sales: 15000 }
];
const generator = new ReportGenerator();
const pdfReport = generator.generate(data, new PDFFormatter());
const excelReport = generator.generate(data, new ExcelFormatter());
const csvReport = generator.generate(data, new CSVFormatter());
const jsonReport = generator.generate(data, new JSONFormatter());Using Abstraction to Achieve OCP
// Shape calculator example
interface Shape {
calculateArea(): number;
}
class Circle implements Shape {
constructor(private radius: number) {}
calculateArea(): number {
return Math.PI * this.radius ** 2;
}
}
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}
calculateArea(): number {
return this.width * this.height;
}
}
class Triangle implements Shape {
constructor(private base: number, private height: number) {}
calculateArea(): number {
return (this.base * this.height) / 2;
}
}
// New shape - no changes to AreaCalculator!
class Hexagon implements Shape {
constructor(private side: number) {}
calculateArea(): number {
return (3 * Math.sqrt(3) * this.side ** 2) / 2;
}
}
// This class never needs to change when adding new shapes
class AreaCalculator {
calculateTotalArea(shapes: Shape[]): number {
return shapes.reduce((total, shape) => total + shape.calculateArea(), 0);
}
displayAreas(shapes: Shape[]): void {
shapes.forEach((shape, index) => {
console.log(`Shape ${index + 1}: ${shape.calculateArea().toFixed(2)} sq units`);
});
}
}
// Usage
const shapes: Shape[] = [
new Circle(5),
new Rectangle(4, 6),
new Triangle(3, 4),
new Hexagon(3)
];
const calculator = new AreaCalculator();
calculator.displayAreas(shapes);
console.log(`Total area: ${calculator.calculateTotalArea(shapes).toFixed(2)}`);Benefits of OCP
Reduces risk: Existing code remains untouched and tested
Easier to maintain: New features don't affect old code
Better testability: Each extension can be tested independently
Flexibility: Easy to add new functionality without breaking existing features
Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free