Open/Closed Principle — Extension Without Modification
Apply the Open/Closed Principle in C#: designing for extension with interfaces and composition, the strategy pattern as OCP in action, extension points for reporting and notification logic, and what OCP is not.
What OCP Means
Open/Closed Principle:
Software entities should be OPEN for extension
but CLOSED for modification.
Open for extension: you can add new behavior
Closed for modification: without changing existing code
Why it matters:
Changing existing code risks breaking existing behavior.
Adding new code (implementing an interface, adding a class) is safer.
OCP pushes toward design where adding a feature = adding a class,
not modifying ten existing files.OCP Violation
// Violates OCP: adding a new report type requires modifying GenerateReport
public sealed class ReportGenerator
{
public byte[] GenerateReport(string reportType, ReportData data)
{
if (reportType == "PDF")
{
// PDF generation logic
return GeneratePdf(data);
}
else if (reportType == "Excel")
{
// Excel generation logic
return GenerateExcel(data);
}
else if (reportType == "CSV")
{
// CSV generation logic
return GenerateCsv(data);
}
// Adding "HTML" requires modifying this method ← OCP violation
throw new ArgumentException($"Unknown report type: {reportType}");
}
}OCP Applied — Strategy Pattern
// Interface: the extension point
public interface IReportGenerator
{
string ReportType { get; }
byte[] Generate(ReportData data);
}
// Each format is a separate class — no modification needed to add a new one
public sealed class PdfReportGenerator : IReportGenerator
{
public string ReportType => "PDF";
public byte[] Generate(ReportData data) { /* PDF logic */ return []; }
}
public sealed class ExcelReportGenerator : IReportGenerator
{
public string ReportType => "Excel";
public byte[] Generate(ReportData data) { /* Excel logic */ return []; }
}
public sealed class CsvReportGenerator : IReportGenerator
{
public string ReportType => "CSV";
public byte[] Generate(ReportData data) { /* CSV logic */ return []; }
}
// Dispatcher: closed for modification, open for extension (new generator = new class)
public sealed class ReportService
{
private readonly IReadOnlyDictionary<string, IReportGenerator> _generators;
public ReportService(IEnumerable<IReportGenerator> generators)
=> _generators = generators.ToDictionary(g => g.ReportType);
public byte[] Generate(string reportType, ReportData data)
{
if (!_generators.TryGetValue(reportType, out var generator))
throw new ArgumentException($"Report type '{reportType}' not supported.");
return generator.Generate(data);
}
}
// Adding HTML: create HtmlReportGenerator : IReportGenerator, register in DI.
// No existing code changes.Clinical Example: Notification Channels
// Prescription alert notifications — OCP violation:
// Every new channel requires modifying SendAlert
public class AlertService
{
public void SendAlert(string channel, string patientMrn, string message)
{
if (channel == "SMS") { /* SMS logic */ }
else if (channel == "Email") { /* Email logic */ }
else if (channel == "Pager") { /* Pager logic */ }
// Adding "Teams" or "PushNotification" modifies this ← violation
}
}
// OCP-compliant: each channel is a separate implementation
public interface IAlertChannel
{
string Channel { get; }
Task SendAsync(PatientAlert alert, CancellationToken ct);
}
public sealed class SmsAlertChannel : IAlertChannel
{
public string Channel => "SMS";
public async Task SendAsync(PatientAlert alert, CancellationToken ct)
=> await _smsService.SendAsync(alert.PhoneNumber, alert.Message, ct);
}
public sealed class EmailAlertChannel : IAlertChannel
{
public string Channel => "Email";
public async Task SendAsync(PatientAlert alert, CancellationToken ct)
=> await _emailService.SendAsync(alert.EmailAddress, alert.Message, ct);
}
// AlertDispatcher is closed — adding Teams requires a new class, not a modification
public sealed class AlertDispatcher
{
private readonly IEnumerable<IAlertChannel> _channels;
public async Task SendAsync(PatientAlert alert, CancellationToken ct)
{
var channel = _channels.FirstOrDefault(c => c.Channel == alert.PreferredChannel);
if (channel is not null)
await channel.SendAsync(alert, ct);
}
}What OCP Is NOT
OCP is not:
✗ "Never change any code" — changing a bug fix or refactoring is fine
✗ "Always add abstractions" — don't add interfaces for everything
✗ An excuse for over-engineering — don't add extension points you don't need
OCP is:
✓ Identify where change is likely (new report types, new notification channels)
✓ Design extension points for those areas (interfaces, abstract base classes)
✓ Leave stable logic closed (don't change it when extensions are added)
Apply OCP where you expect extension.
Not everywhere — premature extension points are complexity without benefit.Production issue I've seen: A clinical alert service had
if (alertType == "CriticalINR") { ... } else if (alertType == "MedicationDue") { ... } else if (alertType == "FallRisk") { ... }— 14 else-if branches in one method. Every sprint added a new alert type, and every developer modified the same 200-line method, causing merge conflicts. Refactoring toIAlertTypeHandlerwith 14 separate classes eliminated the conflicts: each sprint's new alert type became a new file, never touching existing ones.
Key Takeaway
OCP means extending behavior by adding new code, not modifying existing code. The Strategy pattern is OCP in action: register implementations of an interface, add new behavior by adding a new class. Identify where your codebase has
switchorif/elsechains that grow with every new requirement — those are OCP violations waiting to cause merge conflicts and regression bugs.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.