Learnixo
Back to blog
AI Systemsintermediate

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.

Asma Hafeez KhanMay 16, 20264 min read
SOLIDOCPDesign PrinciplesC#.NETArchitecture
Share:𝕏

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

C#
// 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

C#
// 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

C#
// 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 to IAlertTypeHandler with 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 switch or if/else chains that grow with every new requirement — those are OCP violations waiting to cause merge conflicts and regression bugs.

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.