Solid
02 / 05

Open/Closed Principle

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 isolation

The 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