Learn Open/Closed Principle (OCP)

Introduction

In this article, we will cover the Open/Closed Principle (OCP).

Open/Closed Principle (OCP)

As stated by Bertrand Meyer, "software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification." What does that mean though? This principle states that you should allow users to add new functionalities without changing existing code.

Let’s take an example of the Bank Payment Gateway System.

Imagine you’re building an e-commerce application where customers can make payments using various methods: credit cards, PayPal, or bank transfers. As the e-commerce industry evolves, you anticipate supporting more payment methods.

Violation of OCP / Bad Code

using System;
namespace DesignPattern
{
    public enum PaymentMethod
    {
        CreditCard,
        PayPal,
        BankTransfer
    }

    public class PaymentProcessor
    {
        public void ProcessPayment(PaymentMethod method, double amount)
        {
            switch (method)
            {
                case PaymentMethod.CreditCard:
                    Console.WriteLine($"Processing credit card payment for amount: ${amount}");
                    break;

                case PaymentMethod.PayPal:
                    Console.WriteLine($"Processing PayPal payment for amount: ${amount}");
                    break;

                case PaymentMethod.BankTransfer:
                    Console.WriteLine($"Processing bank transfer for amount: ${amount}");
                    break;

                default:
                    throw new ArgumentException("Unsupported payment method");
            }
        }
    }
}

Note. The problem with this design is that each time a new payment method is added, we have to modify the PaymentProcessor class.

Using OCP / Good Code

using System;
namespace DesignPattern
{
    //Define an interface for processing payments
    public interface IPaymentProcessor
    {
        void ProcessPayment(double amount);
    }

    //Implement this interface for each payment method
    public class CreditCardPayment : IPaymentProcessor
    {
        public void ProcessPayment(double amount)
        {
            Console.WriteLine($"Processing credit card payment for amount: ${amount}");
        }
    }

    public class PayPalPayment : IPaymentProcessor
    {
        public void ProcessPayment(double amount)
        {
            Console.WriteLine($"Processing PayPal payment for amount: ₹{amount}");
        }
    }

    public class BankTransferPayment : IPaymentProcessor
    {
        public void ProcessPayment(double amount)
        {
            Console.WriteLine($"Processing bank transfer for amount: ₹{amount}");
        }
    }

    //Modify the main payment handler
    public class PaymentHandler
    {
        private readonly IPaymentProcessor _paymentProcessor;

        public PaymentHandler(IPaymentProcessor paymentProcessor)
        {
            _paymentProcessor = paymentProcessor;
        }

        public void ExecutePayment(double amount)
        {
            _paymentProcessor.ProcessPayment(amount);
        }
    }
    
    //Testing the Open-Closed Principle
    public class Program
    {
        public static void Main()
        {
            var creditCardPayment = new CreditCardPayment();
            var handler = new PaymentHandler(creditCardPayment);
            handler.ExecutePayment(100);

            var paypalPayment = new PayPalPayment();
            handler = new PaymentHandler(paypalPayment);
            handler.ExecutePayment(250);
            
            Console.ReadKey();
        }
    }
}

OCP

Note: With this design, when we need to add a new payment method (e.g., a crypto payment), we create a new class (e.g., CryptoPayment) that implements IPaymentProcessor. There’s no need to modify the existing PaymentHandler or other payment classes. This design is open for extension but closed for modification.