--- id: solid-ocp-abstraction title: SOLID - Open/Closed (Abstraction) category: solid-principles priority: critical tags: [SOLID, OCP, open-closed, abstraction] related: [solid-ocp-extension, solid-dip-abstractions, pattern-repository] --- # Open/Closed Principle - Abstraction Use abstractions (interfaces and abstract classes) to define stable contracts that allow new implementations without modifying existing code. ## Bad Example ```typescript // Anti-pattern: Concrete dependencies that require modification for new requirements class ReportGenerator { generateReport(data: SalesData[], format: string): string { let report = ''; // Calculate totals const totalSales = data.reduce((sum, d) => sum + d.amount, 0); const averageSale = totalSales / data.length; if (format === 'html') { report = `

Sales Report

Total Sales: $${totalSales}

Average Sale: $${averageSale.toFixed(2)}

${data.map(d => ``).join('')}
${d.date}$${d.amount}
`; } else if (format === 'pdf') { // PDF generation logic mixed in const pdf = new PDFDocument(); pdf.text('Sales Report'); pdf.text(`Total: $${totalSales}`); // ... more PDF logic report = pdf.output(); } else if (format === 'csv') { report = 'Date,Amount\n'; report += data.map(d => `${d.date},${d.amount}`).join('\n'); } else if (format === 'excel') { // Excel generation logic // Adding new format requires modifying this class } return report; } } // Problems: // 1. Adding JSON format requires modifying ReportGenerator // 2. All format logic is tightly coupled // 3. Testing is difficult // 4. No way to extend without modification ``` ## Good Example ```typescript // Correct approach: Abstractions enable extension without modification // Stable abstraction for report data interface ReportData { readonly title: string; readonly generatedAt: Date; readonly summary: ReportSummary; readonly items: ReportItem[]; } interface ReportSummary { readonly totalSales: number; readonly averageSale: number; readonly itemCount: number; } interface ReportItem { readonly date: string; readonly amount: number; readonly description?: string; } // Stable abstraction for formatters interface ReportFormatter { readonly format: string; readonly mimeType: string; render(data: ReportData): string | Buffer | Promise; } // Stable abstraction for data sources interface ReportDataSource { fetch(criteria: ReportCriteria): Promise; transform(rawData: T[]): ReportData; } // Report generator depends only on abstractions class ReportGenerator { constructor( private formatters: Map = new Map() ) {} registerFormatter(formatter: ReportFormatter): void { this.formatters.set(formatter.format, formatter); } async generate( dataSource: ReportDataSource, criteria: ReportCriteria, format: string ): Promise { const formatter = this.formatters.get(format); if (!formatter) { throw new UnsupportedFormatError(format, this.getSupportedFormats()); } const rawData = await dataSource.fetch(criteria); const reportData = dataSource.transform(rawData); const content = await Promise.resolve(formatter.render(reportData)); return { content, mimeType: formatter.mimeType, format, generatedAt: new Date() }; } getSupportedFormats(): string[] { return Array.from(this.formatters.keys()); } } // Concrete formatters - each can be added without modifying ReportGenerator class HtmlReportFormatter implements ReportFormatter { readonly format = 'html'; readonly mimeType = 'text/html'; render(data: ReportData): string { return ` ${data.title}

${data.title}

Generated: ${data.generatedAt.toISOString()}

Total Sales: $${data.summary.totalSales.toFixed(2)}

Average Sale: $${data.summary.averageSale.toFixed(2)}

${data.items.map(item => `` ).join('')}
DateAmount
${item.date}$${item.amount}
`; } } class CsvReportFormatter implements ReportFormatter { readonly format = 'csv'; readonly mimeType = 'text/csv'; render(data: ReportData): string { const header = 'Date,Amount,Description'; const rows = data.items.map(item => `${item.date},${item.amount},"${item.description || ''}"` ); return [header, ...rows].join('\n'); } } class JsonReportFormatter implements ReportFormatter { readonly format = 'json'; readonly mimeType = 'application/json'; render(data: ReportData): string { return JSON.stringify(data, null, 2); } } // Adding Excel format - no modification to existing code class ExcelReportFormatter implements ReportFormatter { readonly format = 'xlsx'; readonly mimeType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; render(data: ReportData): Buffer { const workbook = new ExcelJS.Workbook(); const sheet = workbook.addWorksheet(data.title); sheet.addRow(['Date', 'Amount', 'Description']); data.items.forEach(item => { sheet.addRow([item.date, item.amount, item.description || '']); }); return workbook.xlsx.writeBuffer(); } } // Concrete data source - can create new sources without modification class SalesReportDataSource implements ReportDataSource { constructor(private salesRepository: SalesRepository) {} async fetch(criteria: ReportCriteria): Promise { return this.salesRepository.findByCriteria(criteria); } transform(rawData: SalesData[]): ReportData { const totalSales = rawData.reduce((sum, d) => sum + d.amount, 0); return { title: 'Sales Report', generatedAt: new Date(), summary: { totalSales, averageSale: rawData.length > 0 ? totalSales / rawData.length : 0, itemCount: rawData.length }, items: rawData.map(d => ({ date: d.date, amount: d.amount, description: d.productName })) }; } } // Usage const generator = new ReportGenerator(); generator.registerFormatter(new HtmlReportFormatter()); generator.registerFormatter(new CsvReportFormatter()); generator.registerFormatter(new JsonReportFormatter()); generator.registerFormatter(new ExcelReportFormatter()); const salesDataSource = new SalesReportDataSource(salesRepository); const report = await generator.generate(salesDataSource, criteria, 'xlsx'); ``` ## Why 1. **Stable Core**: The `ReportGenerator` class and interfaces form a stable core that rarely changes. 2. **Independent Development**: Teams can develop new formatters or data sources independently. 3. **Composition Over Inheritance**: New functionality is added through composition (registering new implementations) rather than inheritance hierarchies. 4. **Testing**: Each component can be tested in isolation with mock implementations of interfaces. 5. **Flexibility**: The same generator works with any combination of data sources and formatters. 6. **Dependency Inversion**: High-level modules (ReportGenerator) don't depend on low-level modules (specific formatters), both depend on abstractions. 7. **Plugin System**: Natural foundation for a plugin architecture where third parties can add formatters.