Open-Closed Principle in C#

Open-Closed Principle in C# with Examples

In this article, I will discuss the Open-Closed Principle in C# with Examples. Please read our previous article before proceeding to this article, where we discussed the Single Responsibility Principle in C# with Real-Time Examples. The letter O in SOLID stands for the Open-Closed Principle, or OCP. As part of this article, we will discuss the following pointers in detail.

  1. What is the Open-Closed Principle in C#?
  2. Implementation Guidelines for the Open-Closed Principle in C#
  3. What Problems will you get if you are not following the Open-Closed Principle?
  4. Example without using the Open-Closed Principle.
  5. Example using the Open-Closed Principle in C#.
  6. Advantages and Disadvantages of OCP.
  7. Real-Time Example of OCP.
What is the Open-Closed Principle in C#?

The Open-Closed Principle States that Software entities, such as modules, classes, functions, etc., should be open for Extension but closed for Modification.

Let us understand the above definition in simple words. Here, we need to understand two things. The first is Open for Extension, and the second is Closed for Modification. The Open for Extension means we must design the software modules/classes/functions so that the new responsibilities or functionalities can be added easily when new requirements come. On the other hand, Closed for Modification means we should not modify the class/module/function until we find some bugs.

We have already developed a Class/Module/Function, which has undergone the unit testing phase. So we should not have to change this as it affects the existing functionalities. In simple words, we can say that we should develop one Class/Module/Function in such a way that it should allow its behavior to be extended without altering its source code. That means we should not edit the code of a method (until we find some bugs); instead, we should use polymorphism or other techniques to add new functionality by writing new code.

Implementation Guidelines for Open-Closed Principle (OCP) using C#
  1. The easiest way to implement the Open-Closed Principle in C# is to add new functionalities by creating new derived classes, which should be inherited from the original base class.
  2. Another way is to allow the client to access the original class with an abstract interface. 

So, when a requirement change or any new requirement comes at any given time, instead of touching the existing functionality, it’s always better and suggested to create new derived classes and leave the original class implementation as it is. Let us understand this with an example. First, we will see the example of not following the Open-Close Principle. We will understand the problems if we are not following the OCP. Finally, we will see the same example by following the Open-Close Principle using C# Language so that you will get a better idea of this Principle.

Example to Understand Open-Closed Principle in C#.

Please have a look at the following class. As you can see in the image below, we have created the GetInvoiceDiscount() method within the Invoice class. As part of that GetInvoiceDiscount() method, we calculate the final amount based on the Invoice type. Currently, we have two Invoice Types, i.e., Final Invoice and Proposed Invoice. So, we have implemented the logic using the if-else condition. 

Violating open Closed principle in C#

Tomorrow, if one more Invoice Type comes into the picture, we need to modify the GetInvoiceDiscount() method logic by adding another if block to the source code. Changing the source code for the new requirement violates the Open-Closed Principle in C#. 

Example Without Using Open-Closed Principle in C#:
namespace SOLID_PRINCIPLES.OCP
{
    public class Invoice
    {        
        public double GetInvoiceDiscount(double amount, InvoiceType invoiceType)
        {
            double finalAmount = 0;
            if (invoiceType == InvoiceType.FinalInvoice)
            {
                finalAmount = amount - 100;
            }
            else if (invoiceType == InvoiceType.ProposedInvoice)
            {
                finalAmount = amount - 50;
            }
            return finalAmount;
        }
    }
    public enum InvoiceType
    {
        FinalInvoice,
        ProposedInvoice
    };
}

The problem with the above example is that if we want to add another new invoice type, we need to add one more “else if” condition in the same “GetInvoiceDiscount” method. In other words, we need to modify the Invoice class GetInvoiceDiscount Method. Suppose we are changing the Invoice class GetInvoiceDiscount Method again and again. In that case, we need to ensure that the previous and new functionalities are working properly by testing the existing functionalities again. This is because we need to ensure that the existing clients referencing this class are working properly as expected.

Problems of Not Following the Open-Closed Principle in C#: 

So, if you are not following the Open-Closed Principle during the application development process, then you may end up with your application development with the following problems

  1. If you allow a class or function to add new logic, then as a developer, you need to test the entire functionalities, including the old and new functionalities of the application.
  2. As a developer, it is also your responsibility to tell the QA (Quality Assurance) team about the changes in advance so that they can prepare themselves for regression testing along with the new feature testing.
  3. If you are not following the Open-Closed Principle, it also breaks the Single Responsibility Principle, as the class or module will perform multiple responsibilities.
  4. If you are implementing all the functionalities in a single class, then the maintenance of the class becomes very difficult.

Because of the above key points, we must follow the open-closed principle in C# while developing the application.

Open-Closed Principle in C#

We should go for EXTENSION instead of MODIFICATION as per the Open-Closed principle. If you want to follow the Open-Closed Principle in the above example, we need to add a new class when a new invoice type needs to be added. As a result, the current functionalities that are already implemented are going to be unchanged. The advantage is that we only need to test and check the new classes.

Example following Open-Closed Principle in C#

The following example shows the Open Closed Principle (OCP) in C#. As you can see in the code below, we have created three classes: FinalInvoiceProposedInvoice, and RecurringInvoice. All these three classes are inherited from the base class Invoice, and if they want, they can override the GetInvoiceDiscount() method, which is declared as Virtual in the Base Invoice class. 

namespace SOLID_PRINCIPLES.OCP
{
    public class Invoice
    {
        public virtual double GetInvoiceDiscount(double amount)
        {
            return amount - 10;
        }
    }
    
    public class FinalInvoice : Invoice
    {
        public override double GetInvoiceDiscount(double amount)
        {
            return base.GetInvoiceDiscount(amount) - 50;
        }
    }
    public class ProposedInvoice : Invoice
    {
        public override double GetInvoiceDiscount(double amount)
        {
            return base.GetInvoiceDiscount(amount) - 40;
        }
    }
    public class RecurringInvoice : Invoice
    {
        public override double GetInvoiceDiscount(double amount)
        {
            return base.GetInvoiceDiscount(amount) - 30;
        }
    }
}

Tomorrow, if another Invoice Type needs to be added, we need to create a new class by inheriting it from the Invoice class, and if needed, then we need to override the GetInvoiceDiscount() method. The point that you need to keep focus on is we are not changing the code of the Invoice class. Now, the Invoice class is Closed for Modification. But it is Open for Extension as it allows the creation of new classes deriving from the base Invoice class, which follows the Open-Closed Principle in C#. 

Now, modify the Main method of the Program class as shown below to test the application and see whether the different Invoice types are working as expected or not as per our business requirement.

using System;
namespace SOLID_PRINCIPLES.OCP
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Invoice Amount: 10000");

            Invoice FInvoice = new FinalInvoice();
            double FInvoiceAmount = FInvoice.GetInvoiceDiscount(10000);
            Console.WriteLine($"Final Invoive : {FInvoiceAmount}");

            Invoice PInvoice = new ProposedInvoice();
            double PInvoiceAmount = PInvoice.GetInvoiceDiscount(10000);
            Console.WriteLine($"Proposed Invoive : {PInvoiceAmount}");

            Invoice RInvoice = new RecurringInvoice();
            double RInvoiceAmount = RInvoice.GetInvoiceDiscount(10000);
            Console.WriteLine($"Recurring Invoive : {RInvoiceAmount}");
            Console.ReadKey();
        }
    }
}

When you run the above application, you will get the following output as expected.

Open-Closed Principle in C# with Examples

Here’s a simple example demonstrating using the Open-Closed Principle in C# using the Strategy pattern. In the below example, the PaymentProcessor class is open for extension by accepting different payment strategies (CreditCardPaymentStrategy, PayPalPaymentStrategy) without modifying its code. New payment strategies can be added without changing the existing payment processing logic.

namespace OCPPrinciple
{
    // Abstract strategy
    public interface IPaymentStrategy
    {
        void ProcessPayment(decimal amount);
    }

    // Concrete strategies
    public class CreditCardPaymentStrategy : IPaymentStrategy
    {
        public void ProcessPayment(decimal amount)
        {
            // Process credit card payment
        }
    }

    public class PayPalPaymentStrategy : IPaymentStrategy
    {
        public void ProcessPayment(decimal amount)
        {
            // Process PayPal payment
        }
    }

    // Context class
    public class PaymentProcessor
    {
        private readonly IPaymentStrategy _paymentStrategy;

        public PaymentProcessor(IPaymentStrategy paymentStrategy)
        {
            _paymentStrategy = paymentStrategy;
        }

        public void ProcessPayment(decimal amount)
        {
            _paymentStrategy.ProcessPayment(amount);
        }
    }
}

In the next article, I will discuss Multiple Real-Time Examples of the Open-Closed Principle (OCP) in C#. In this article, I explain the Open-Closed Principle in C# with Examples. I hope you understand the need and use of the Open-Closed Principle in C# with Examples.

17 thoughts on “Open-Closed Principle in C#”

  1. If we have like 20 different types, should we create 20 different classes? That would be a whole lot of classes.

  2. Author you don’t need that enum no more.
    Also why base method is returning 500 hard coded?
    That doesn’t make any scene.

    1. i think the code need to be modified as below

      public virtual double GetInvoiceDiscount(double amount)
      {
      double finalAmount = amount;
      return finalAmount;
      }

  3. First off . Thanks for providing great content. Can we use Interface(eg . IInvoice) instead of using classes for this example.

    1. Yes, you can also use interface and implement the GetInvoiceDiscount() method in the inheriting classes. I personally prepfer Interface than a virutal method of a class.

  4. Why do we need to create three separate class objects? Can we not cast it to an object like (obj as Invoice) and use it further, such that there will be minimum editing in the Main method?

    1. That is the intended way, yes. The method in which this will happen will most likely take an Invoke object as a parameter. It has been done like that to show the readers how to implement it, and how it will change the workflow.

Leave a Reply

Your email address will not be published. Required fields are marked *