Summary
This article explores the Open/Closed Principle (OCP)—software entities should be open for extension but closed for modification. You'll learn how to design C# applications that accommodate new features without changing existing, working code. Through practical examples, we'll transform rigid, if-else-laden code into flexible, extensible architectures using interfaces, abstract classes, and polymorphism.
Prerequisites
Understanding of C# interfaces and abstract classes
Familiarity with polymorphism and inheritance
Knowledge of Single Responsibility Principle (SRP) recommended
Basic experience with dependency injection helpful
The Open/Closed Principle states: Software entities should be open for extension but closed for modification.
In other words, you should be able to add new functionality without changing existing code. Sounds impossible? It's not. Let's see how.
The Problem: Modifying Existing Code
Imagine you're building a payment processing system. You start with credit cards:
public class PaymentProcessor
{
public void ProcessPayment(string paymentType, decimal amount)
{
if (paymentType == "CreditCard")
{
Console.WriteLine($"Processing credit card payment of ${amount}");
// Credit card processing logic
ValidateCreditCard();
ChargeCreditCard(amount);
}
}
private void ValidateCreditCard() { /* ... */ }
private void ChargeCreditCard(decimal amount) { /* ... */ }
}
This works fine. Then the business wants to accept PayPal. You modify the class:
public class PaymentProcessor
{
public void ProcessPayment(string paymentType, decimal amount)
{
if (paymentType == "CreditCard")
{
Console.WriteLine($"Processing credit card payment of ${amount}");
ValidateCreditCard();
ChargeCreditCard(amount);
}
else if (paymentType == "PayPal")
{
Console.WriteLine($"Processing PayPal payment of ${amount}");
ValidatePayPalAccount();
ChargePayPal(amount);
}
}
private void ValidateCreditCard() { /* ... */ }
private void ChargeCreditCard(decimal amount) { /* ... */ }
private void ValidatePayPalAccount() { /* ... */ }
private void ChargePayPal(decimal amount) { /* ... */ }
}
Now they want cryptocurrency. You modify again:
public class PaymentProcessor
{
public void ProcessPayment(string paymentType, decimal amount)
{
if (paymentType == "CreditCard")
{
// Credit card logic...
}
else if (paymentType == "PayPal")
{
// PayPal logic...
}
else if (paymentType == "Cryptocurrency")
{
Console.WriteLine($"Processing crypto payment of ${amount}");
ValidateCryptoWallet();
TransferCrypto(amount);
}
}
// More and more methods...
}
See the problem? Every new payment method requires modifying PaymentProcessor. This violates OCP. You're changing existing, tested code to add features. Each change risks breaking working functionality.
Refactoring with OCP
Let's make this extensible:
public interface IPaymentMethod
{
void ProcessPayment(decimal amount);
}
public class CreditCardPayment : IPaymentMethod
{
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing credit card payment of ${amount}");
ValidateCard();
ChargeCard(amount);
}
private void ValidateCard() { /* ... */ }
private void ChargeCard(decimal amount) { /* ... */ }
}
public class PayPalPayment : IPaymentMethod
{
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing PayPal payment of ${amount}");
ValidateAccount();
ChargeAccount(amount);
}
private void ValidateAccount() { /* ... */ }
private void ChargeAccount(decimal amount) { /* ... */ }
}
public class CryptocurrencyPayment : IPaymentMethod
{
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing crypto payment of ${amount}");
ValidateWallet();
TransferFunds(amount);
}
private void ValidateWallet() { /* ... */ }
private void TransferFunds(decimal amount) { /* ... */ }
}
public class PaymentProcessor
{
public void ProcessPayment(IPaymentMethod paymentMethod, decimal amount)
{
paymentMethod.ProcessPayment(amount);
}
}
Now adding a new payment method is simple:
public class BankTransferPayment : IPaymentMethod
{
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing bank transfer of ${amount}");
// Bank transfer logic
}
}
No changes to PaymentProcessor. It's closed for modification but open for extension. We added functionality by creating a new class, not modifying existing ones.
The Discount Calculator Problem
Let's look at another common scenario. You're building an e-commerce system with different discount types:
public class DiscountCalculator
{
public decimal CalculateDiscount(string customerType, decimal orderTotal)
{
if (customerType == "Regular")
{
return 0; // No discount
}
else if (customerType == "Silver")
{
return orderTotal * 0.10m; // 10% off
}
else if (customerType == "Gold")
{
return orderTotal * 0.20m; // 20% off
}
else if (customerType == "Platinum")
{
return orderTotal * 0.30m; // 30% off
}
return 0;
}
}
Adding a "Diamond" tier means modifying this method. What if discount rules become more complex? What if Silver customers get 15% off on weekends? The if-else chain grows uncontrollably.
OCP-Compliant Design
public interface IDiscountPolicy
{
decimal CalculateDiscount(decimal orderTotal);
}
public class RegularCustomerDiscount : IDiscountPolicy
{
public decimal CalculateDiscount(decimal orderTotal)
{
return 0; // No discount
}
}
public class SilverCustomerDiscount : IDiscountPolicy
{
public decimal CalculateDiscount(decimal orderTotal)
{
return orderTotal * 0.10m;
}
}
public class GoldCustomerDiscount : IDiscountPolicy
{
public decimal CalculateDiscount(decimal orderTotal)
{
return orderTotal * 0.20m;
}
}
public class PlatinumCustomerDiscount : IDiscountPolicy
{
public decimal CalculateDiscount(decimal orderTotal)
{
return orderTotal * 0.30m;
}
}
public class DiscountCalculator
{
public decimal CalculateDiscount(IDiscountPolicy policy, decimal orderTotal)
{
return policy.CalculateDiscount(orderTotal);
}
}
Want to add Diamond tier? Create a new class:
public class DiamondCustomerDiscount : IDiscountPolicy
{
public decimal CalculateDiscount(decimal orderTotal)
{
return orderTotal * 0.40m;
}
}
Need weekend bonuses for Silver customers? Extend the class:
public class SilverWeekendDiscount : IDiscountPolicy
{
public decimal CalculateDiscount(decimal orderTotal)
{
var baseDiscount = orderTotal * 0.10m;
if (DateTime.Now.DayOfWeek == DayOfWeek.Saturday ||
DateTime.Now.DayOfWeek == DayOfWeek.Sunday)
{
return orderTotal * 0.15m; // 15% on weekends
}
return baseDiscount;
}
}
Your core DiscountCalculator never changes. It's closed for modification.
The Logging System Evolution
Here's a scenario where OCP prevents cascading changes. You start with console logging:

Join the conversation! Your thoughts help the community grow.