Introduction
It says, "Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification."
This means that we should be able to add new functionality to an entity without changing its existing code. Adhering to OCP helps make code more maintainable, less error-prone, and adaptable to new requirements.
In this article, we’ll look at how to implement OCP in C#, with practical examples.
Why the Open-Closed Principle?
When code is “open for extension,” it can be easily adapted to add new features without disturbing existing code. However, being “closed for modification” protects the code from unintended side effects that can arise from altering core logic. This approach improves software stability and makes testing and maintenance easier.
Let’s take a look at some examples to understand how OCP works and how to implement it in C#.
A Non-OCP-Compliant Example
Imagine we’re building a payment processing system. Here’s a simple class that processes payments based on different payment methods:
Bad Code ❌
public class PaymentProcessor
{
public void ProcessPayment(string paymentMethod)
{
if (paymentMethod == "CreditCard")
{
// Process credit card payment
Console.WriteLine("Processing credit card payment...");
}
else if (paymentMethod == "PayPal")
{
// Process PayPal payment
Console.WriteLine("Processing PayPal payment...");
}
else if (paymentMethod == "Bitcoin")
{
// Process Bitcoin payment
Console.WriteLine("Processing Bitcoin payment...");
}
else
{
throw new ArgumentException("Invalid payment method.");
}
}
}
While this code works, it violates OCP because every time we add a new payment method (like GooglePay
or ApplePay
), we’ll have to modify the ProcessPayment
method. This approach is error-prone and can lead to fragile code as the application grows.
Refactoring to Follow the Open-Closed Principle
To refactor this code to follow the Open-Closed Principle, we can use polymorphism. We’ll create an interface called IPaymentMethod
with a ProcessPayment
method. Each payment type will have its own class implementing this interface.
Step 1: Define an Interface
We start by defining an IPaymentMethod
interface:
public interface IPaymentMethod
{
void ProcessPayment();
}
Step 2: Implement Concrete Classes for Each Payment Method
Next, we create separate classes for each payment type that implement IPaymentMethod
:
public class CreditCardPayment : IPaymentMethod
{
public void ProcessPayment()
{
Console.WriteLine("Processing credit card payment...");
}
}
public class PayPalPayment : IPaymentMethod
{
public void ProcessPayment()
{
Console.WriteLine("Processing PayPal payment...");
}
}
public class BitcoinPayment : IPaymentMethod
{
public void ProcessPayment()
{
Console.WriteLine("Processing Bitcoin payment...");
}
}
Step 3: Modify the PaymentProcessor Class
Now, instead of directly handling the payment types, PaymentProcessor
will use the IPaymentMethod
interface. This makes the class open for extension but closed for modification.
public class PaymentProcessor
{
public void ProcessPayment(IPaymentMethod paymentMethod)
{
paymentMethod.ProcessPayment();
}
}
Using the Refactored Code
Let’s see how to use this refactored code:
Good Code ✅
class Program
{
static void Main()
{
PaymentProcessor paymentProcessor = new PaymentProcessor();
IPaymentMethod creditCardPayment = new CreditCardPayment();
IPaymentMethod paypalPayment = new PayPalPayment();
IPaymentMethod bitcoinPayment = new BitcoinPayment();
paymentProcessor.ProcessPayment(creditCardPayment);
paymentProcessor.ProcessPayment(paypalPayment);
paymentProcessor.ProcessPayment(bitcoinPayment);
}
}
With this structure, adding a new payment type (e.g., GooglePayPayment
) only requires creating a new class that implements IPaymentMethod
, without modifying PaymentProcessor
.
Benefits of Following OCP
Improved Flexibility: New functionality can be added without changing existing code, making the codebase more adaptable.
Reduced Errors: With separate classes for each payment method, errors are less likely to propagate to unrelated parts of the code.
Enhanced Maintainability: Code is easier to understand, and maintenance tasks are more manageable.
Facilitated Testing: Each payment type class can be independently tested, simplifying the testing process.
Conclusion
The Open-Closed Principle encourages designing code that is adaptable to change and resistant to breakage. In this article, we’ve covered how to implement OCP in C# with examples, such as payment processing and logging.
By using interfaces and polymorphism, you can make your code open for extension but closed for modification—leading to a more robust, flexible, and maintainable codebase.
A big thank you to you, mate! For reading and supporting. 💜 💖 💛
Top comments (6)
Easy to understand
Thank you!
Cool.
I'm glad that you liked the post :)
Easy to comprehend
@kred12 thanks!