DEV Community

Omar Faruque Shamim
Omar Faruque Shamim

Posted on

Open/Closed Principle (OCP)

Software entities (classes, modules, functions) should be open for extension but closed for modification.

This means:
Open for Extension: You should be able to add new functionality to an existing module or class without altering its existing code.

Closed for Modification: Once a class is written and tested, you should not need to modify it to add new functionality.

Why is OCP Important?

  • Preserves Stability: Existing code remains untouched, reducing the risk of introducing new bugs.

  • Enhances Flexibility: Makes the system adaptable to new requirements without rewriting or breaking existing logic.

  • Supports Scalability: New behaviors can be added with minimal effort.

  • Aligns with Agile Practices: Easily adapts to changing requirements, a common scenario in software development.


How Violates OCP?

OCP is violated when:

  • Adding new behavior requires modifying existing classes or methods.
  • This leads to tight coupling, making it harder to maintain or extend the system.

Example of OCP Violation:
Imagine a PaymentProcessorclass that handles different payment methods:

public class PaymentProcessor
{
    public void ProcessPayment(string paymentType, decimal amount)
    {
        if (paymentType == "CreditCard")
        {
            Console.WriteLine($"Processing credit card payment of {amount:C}.");
        }
        else if (paymentType == "Cash")
        {
            Console.WriteLine($"Processing cash payment of {amount:C}.");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Problems:

  • Adding a new payment type requires modifying the ProcessPayment method.
  • Every change increases the likelihood of bugs in existing functionality.
  • This violates OCP because the class is not closed for modification.

How to Implement Class-Level OCP

OCP can be implemented using techniques like polymorphism, composition, and strategy patterns. Below are steps for implementation using class-level OCP with execution flow.

Step 1: Define an Abstraction
Create an abstract class or interface to define common behavior.

public interface IPaymentProcessor
{
    void ProcessPayment(decimal amount);
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Create Concrete Implementations
Implement the interface for each payment type.

public class CreditCardProcessor : IPaymentProcessor
{
    public void ProcessPayment(decimal amount)
    {
        Console.WriteLine($"Processing credit card payment of {amount:C}.");
    }
}

public class CashPaymentProcessor : IPaymentProcessor
{
    public void ProcessPayment(decimal amount)
    {
        if (amount > 1000)
        {
            Console.WriteLine("Cash payment cannot exceed $1000.");
            return;
        }
        Console.WriteLine($"Processing cash payment of {amount:C}.");
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Use Abstraction in the Client
The client depends on the abstraction (IPaymentProcessor) and can dynamically work with any implementation.

public class PaymentHandler
{
    private readonly IPaymentProcessor _paymentProcessor;

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

    public void HandlePayment(decimal amount)
    {
        _paymentProcessor.ProcessPayment(amount);
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Call PaymentHandler
Create and use the appropriate payment processor at runtime.

var creditCardProcessor = new CreditCardProcessor();
var paymentHandler = new PaymentHandler(creditCardProcessor);
paymentHandler.HandlePayment(500);

var cashProcessor = new CashPaymentProcessor();
var cashPaymentHandler = new PaymentHandler(cashProcessor);
cashPaymentHandler.HandlePayment(1200);
Enter fullscreen mode Exit fullscreen mode

Execution Flow

  • The PaymentHandlerrelies on the IPaymentProcessorinterface.
  • Adding a new payment type (e.g., DigitalWalletProcessor) requires creating a new class that implements IPaymentProcessor.
  • No changes are made to the existing PaymentHandleror other classes.

How to Implement Method-Level OCP

For finer-grained control (specific method behavior), we can use strategies or composition to inject new logic into a method.

Example: Discount Calculation

public interface IDiscountStrategy
{
    decimal CalculateDiscount(decimal price);
}

public class RegularDiscount : IDiscountStrategy
{
    public decimal CalculateDiscount(decimal price) => price * 0.1m; // 10% discount
}

public class VIPDiscount : IDiscountStrategy
{
    public decimal CalculateDiscount(decimal price) => price * 0.2m; // 20% discount
}

public class DiscountCalculator
{
    public decimal Calculate(decimal price, IDiscountStrategy discountStrategy)
    {
        return discountStrategy.CalculateDiscount(price);
    }
}

Enter fullscreen mode Exit fullscreen mode

Usage:

var calculator = new DiscountCalculator();
decimal discountedPrice = calculator.Calculate(1000, new VIPDiscount());
Console.WriteLine($"Discounted Price: {discountedPrice:C}");

Enter fullscreen mode Exit fullscreen mode

Flow:
New discount types can be added without modifying the DiscountCalculator.

By following OCP, you build systems that are easier to extend and maintain while reducing risks of breaking existing functionality.

Top comments (0)