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:
- Use abstractions (interfaces) to define extension points
- Strategy pattern for interchangeable algorithms
- Decorator pattern for adding behavior
- Factory pattern for object creation
- 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