DEV Community

Cover image for SOLID: Open-Closed Principle in C#
Aditya Sharma
Aditya Sharma

Posted on

SOLID: Open-Closed Principle in C#

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.

Open closed

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.");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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...");
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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

  1. Improved Flexibility: New functionality can be added without changing existing code, making the codebase more adaptable.

  2. Reduced Errors: With separate classes for each payment method, errors are less likely to propagate to unrelated parts of the code.

  3. Enhanced Maintainability: Code is easier to understand, and maintenance tasks are more manageable.

  4. 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. 💜 💖 💛

Open closed

Top comments (2)

Collapse
 
kred12 profile image
Ssezooba

Easy to comprehend

Collapse
 
kred12 profile image
Ssezooba

Easy to understand