đź“„

Open/Closed Principle (OCP)

Intermediate 2 min read 300 words

Software entities should be open for extension but closed for modification

Open/Closed Principle (OCP)

“Software entities (classes, modules, functions) should be open for extension, but closed for modification.” — Bertrand Meyer

The Open/Closed Principle means you should be able to add new functionality without changing existing code. This protects working, tested code from introducing bugs.

Understanding OCP

  • Open for extension: You can add new behavior
  • Closed for modification: You don’t change existing code to add new behavior

The key is using abstractions (interfaces, abstract classes) that define how the system can be extended.

Violation Example

// BAD: Must modify class to add new notification types
public class NotificationService
{
    public void SendNotification(string type, string message, string recipient)
    {
        if (type == "email")
        {
            SendEmail(recipient, message);
        }
        else if (type == "sms")
        {
            SendSMS(recipient, message);
        }
        else if (type == "push")
        {
            SendPushNotification(recipient, message);
        }
        // NEW REQUIREMENT: Add Slack notifications
        else if (type == "slack")  // Must modify existing class!
        {
            SendSlack(recipient, message);
        }
        // NEW REQUIREMENT: Add Teams notifications
        else if (type == "teams")  // Must modify again!
        {
            SendTeams(recipient, message);
        }
        // This doesn't scale!
    }

    private void SendEmail(string recipient, string message) { /* ... */ }
    private void SendSMS(string recipient, string message) { /* ... */ }
    private void SendPushNotification(string recipient, string message) { /* ... */ }
    private void SendSlack(string recipient, string message) { /* ... */ }
    private void SendTeams(string recipient, string message) { /* ... */ }
}

Problems:

  • Every new notification type requires modifying this class
  • Risk of breaking existing functionality
  • Class grows larger and harder to test
  • Violates SRP (handles all notification types)

Applying OCP

Use abstraction to allow extension without modification:

// Define an abstraction for notification channels
public interface INotificationChannel
{
    string ChannelType { get; }
    Task SendAsync(string recipient, string message);
    bool CanHandle(string type);
}

// Email implementation
public class EmailChannel : INotificationChannel
{
    private readonly IEmailSender _sender;

    public EmailChannel(IEmailSender sender)
    {
        _sender = sender;
    }

    public string ChannelType => "email";

    public bool CanHandle(string type) => type.Equals("email", StringComparison.OrdinalIgnoreCase);

    public async Task SendAsync(string recipient, string message)
    {
        await _sender.SendAsync(recipient, "Notification", message);
    }
}

// SMS implementation
public class SmsChannel : INotificationChannel
{
    private readonly ISmsGateway _gateway;

    public SmsChannel(ISmsGateway gateway)
    {
        _gateway = gateway;
    }

    public string ChannelType => "sms";

    public bool CanHandle(string type) => type.Equals("sms", StringComparison.OrdinalIgnoreCase);

    public async Task SendAsync(string recipient, string message)
    {
        await _gateway.SendAsync(recipient, message);
    }
}

// Push notification implementation
public class PushChannel : INotificationChannel
{
    private readonly IPushNotificationService _pushService;

    public PushChannel(IPushNotificationService pushService)
    {
        _pushService = pushService;
    }

    public string ChannelType => "push";

    public bool CanHandle(string type) => type.Equals("push", StringComparison.OrdinalIgnoreCase);

    public async Task SendAsync(string recipient, string message)
    {
        await _pushService.SendAsync(recipient, message);
    }
}
// NotificationService is now closed for modification
public class NotificationService
{
    private readonly IEnumerable<INotificationChannel> _channels;
    private readonly ILogger<NotificationService> _logger;

    public NotificationService(
        IEnumerable<INotificationChannel> channels,
        ILogger<NotificationService> logger)
    {
        _channels = channels;
        _logger = logger;
    }

    public async Task SendAsync(string type, string recipient, string message)
    {
        var channel = _channels.FirstOrDefault(c => c.CanHandle(type));

        if (channel == null)
        {
            _logger.LogWarning("No channel found for type: {Type}", type);
            throw new NotSupportedException($"Notification type '{type}' is not supported");
        }

        await channel.SendAsync(recipient, message);
        _logger.LogInformation("Notification sent via {Channel}", channel.ChannelType);
    }

    public async Task SendToAllChannelsAsync(string recipient, string message)
    {
        var tasks = _channels.Select(c => c.SendAsync(recipient, message));
        await Task.WhenAll(tasks);
    }
}
// NEW REQUIREMENT: Add Slack - No modification to NotificationService!
public class SlackChannel : INotificationChannel
{
    private readonly ISlackClient _client;

    public SlackChannel(ISlackClient client)
    {
        _client = client;
    }

    public string ChannelType => "slack";

    public bool CanHandle(string type) => type.Equals("slack", StringComparison.OrdinalIgnoreCase);

    public async Task SendAsync(string recipient, string message)
    {
        await _client.PostMessageAsync(recipient, message);
    }
}

// Just register in DI container
services.AddScoped<INotificationChannel, SlackChannel>();
// NotificationService automatically picks it up!

OCP with Strategy Pattern

// Pricing strategies that can be extended
public interface IPricingStrategy
{
    string Name { get; }
    decimal CalculatePrice(Product product, int quantity);
}

public class RegularPricing : IPricingStrategy
{
    public string Name => "Regular";
    public decimal CalculatePrice(Product product, int quantity)
        => product.Price * quantity;
}

public class BulkPricing : IPricingStrategy
{
    public string Name => "Bulk";
    public decimal CalculatePrice(Product product, int quantity)
    {
        var discount = quantity >= 100 ? 0.20m :
                       quantity >= 50 ? 0.15m :
                       quantity >= 10 ? 0.10m : 0m;

        return product.Price * quantity * (1 - discount);
    }
}

public class MemberPricing : IPricingStrategy
{
    private readonly MembershipLevel _level;

    public MemberPricing(MembershipLevel level)
    {
        _level = level;
    }

    public string Name => $"Member ({_level})";
    public decimal CalculatePrice(Product product, int quantity)
    {
        var discount = _level switch
        {
            MembershipLevel.Gold => 0.25m,
            MembershipLevel.Silver => 0.15m,
            MembershipLevel.Bronze => 0.05m,
            _ => 0m
        };

        return product.Price * quantity * (1 - discount);
    }
}

// NEW: Seasonal pricing - no changes to existing code
public class SeasonalPricing : IPricingStrategy
{
    public string Name => "Seasonal";
    public decimal CalculatePrice(Product product, int quantity)
    {
        var discount = DateTime.Now.Month switch
        {
            12 => 0.25m, // Christmas
            7 or 8 => 0.15m, // Summer sale
            _ => 0m
        };

        return product.Price * quantity * (1 - discount);
    }
}

// Pricing service - closed for modification
public class PricingService
{
    public decimal CalculatePrice(
        IPricingStrategy strategy,
        Product product,
        int quantity)
    {
        return strategy.CalculatePrice(product, quantity);
    }
}

OCP with Decorator Pattern

public interface IDataRepository<T>
{
    Task<T> GetByIdAsync(int id);
    Task SaveAsync(T entity);
}

// Base implementation
public class DatabaseRepository<T> : IDataRepository<T>
{
    private readonly DbContext _context;

    public DatabaseRepository(DbContext context) => _context = context;

    public async Task<T> GetByIdAsync(int id)
        => await _context.Set<T>().FindAsync(id);

    public async Task SaveAsync(T entity)
    {
        _context.Set<T>().Update(entity);
        await _context.SaveChangesAsync();
    }
}

// Extend with caching - without modifying base
public class CachedRepository<T> : IDataRepository<T>
{
    private readonly IDataRepository<T> _inner;
    private readonly IDistributedCache _cache;

    public CachedRepository(IDataRepository<T> inner, IDistributedCache cache)
    {
        _inner = inner;
        _cache = cache;
    }

    public async Task<T> GetByIdAsync(int id)
    {
        var cacheKey = $"{typeof(T).Name}_{id}";
        var cached = await _cache.GetAsync<T>(cacheKey);

        if (cached != null)
            return cached;

        var result = await _inner.GetByIdAsync(id);
        await _cache.SetAsync(cacheKey, result, TimeSpan.FromMinutes(5));
        return result;
    }

    public async Task SaveAsync(T entity)
    {
        await _inner.SaveAsync(entity);
        // Invalidate cache...
    }
}

// Extend with logging - without modifying base
public class LoggingRepository<T> : IDataRepository<T>
{
    private readonly IDataRepository<T> _inner;
    private readonly ILogger _logger;

    public LoggingRepository(IDataRepository<T> inner, ILogger logger)
    {
        _inner = inner;
        _logger = logger;
    }

    public async Task<T> GetByIdAsync(int id)
    {
        _logger.LogDebug("Getting {Type} with id {Id}", typeof(T).Name, id);
        var result = await _inner.GetByIdAsync(id);
        _logger.LogDebug("Retrieved {Type}", typeof(T).Name);
        return result;
    }

    public async Task SaveAsync(T entity)
    {
        _logger.LogDebug("Saving {Type}", typeof(T).Name);
        await _inner.SaveAsync(entity);
        _logger.LogDebug("Saved {Type}", typeof(T).Name);
    }
}

// Compose behaviors without changing any class
IDataRepository<Product> repo = new DatabaseRepository<Product>(context);
repo = new CachedRepository<Product>(repo, cache);
repo = new LoggingRepository<Product>(repo, logger);

Benefits of OCP

Benefit Description
Stability Existing code remains unchanged and tested
Scalability New features are isolated additions
Maintainability Changes don’t ripple through the system
Team Collaboration New features can be developed independently

Interview Tips

Common Questions:

  • “Explain OCP with an example”
  • “How do you add new features without modifying existing code?”
  • “What patterns help achieve OCP?”

Key Points:

  1. Use abstractions (interfaces) to define extension points
  2. Strategy pattern for interchangeable algorithms
  3. Decorator pattern for adding behavior
  4. Factory pattern for object creation
  5. Dependency injection to wire implementations

Remember:

  • OCP doesn’t mean never modify code - it’s about design
  • Initial abstraction might need changes (that’s okay)
  • Balance: don’t over-abstract for hypothetical needs