Understanding the Open/Closed Principle (OCP) with C#

In our previous post, we explored the Single Responsibility Principle (SRP), the first of the SOLID principles, which guides us in writing more maintainable and robust code. In this post, we delve into the second principle: the Open/Closed Principle (OCP). Understanding and implementing OCP is crucial for creating software systems that are both flexible and resilient to change.

Open Closed Principle

The Open/Closed Principle states.

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

In simpler terms, this means that you should be able to add new functionality without changing the existing code. In practice, this often means favoring inheritance and composition over modification when adding new features to your application. By creating abstract classes or interfaces, you can define a contract for how certain methods should behave, allowing other classes to implement those methods in their own way. This allows for flexibility and extensibility while still maintaining a clear structure for your codebase.

Example Scenario

Let's consider a library loan management system that needs to handle different types of items such as books, DVDs, and magazines. The system should calculate the loan period for each item type, which varies depending on the item:

  • Books: 14-day loan period
  • DVDs: 7-day loan period
  • Magazines: 3-day loan period

Initially, our system only supports books, but we need to ensure that it can be extended easily in the future to accommodate other item types without modifying the core logic.

Initial Implementation (Violating OCP)

Let’s start with an implementation that violates the Open/Closed Principle.

public class LoanManager
{
    public double CalculateLoanPeriod(string itemType)
    {
        if (itemType == "Book")
        {
            return 14; // 14 days loan period for books
        }
        else if (itemType == "DVD")
        {
            return 7; // 7 days loan period for DVDs
        }
        else if (itemType == "Magazine")
        {
            return 3; // 3 days loan period for magazines
        }
        else
        {
            throw new ArgumentException("Unknown item type");
        }
    }
}

In this code, the LoanManager class directly handles different item types. Whenever a new item type is introduced, we need to modify the CalculateLoanPeriod method. This approach violates OCP because the class is not closed for modification.

Refactoring to Follow OCP

To adhere to the Open/Closed Principle, we can refactor the code by introducing an interface that each item type will implement. This way, when we need to add a new item type, we extend the system by creating a new class rather than modifying existing code.

// Step 1: Define an interface for loanable items
public interface ILoanableItem
{
    double GetLoanPeriod();
}
// Step 2: Implement concrete classes for each item type
public class Book : ILoanableItem
{
    public double GetLoanPeriod()
    {
        return 14;
    }
}
public class DVD : ILoanableItem
{
    public double GetLoanPeriod()
    {
        return 7;
    }
}
public class Magazine : ILoanableItem
{
    public double GetLoanPeriod()
    {
        return 3;
    }
}
// Step 3: Refactor LoanManager to depend on the ILoanableItem interface
public class LoanManager
{
    public double CalculateLoanPeriod(ILoanableItem item)
    {
        return item.GetLoanPeriod();
    }
}

Now, if we need to add support for a new item type, such as an Audiobook with a loan period of 21 days, we simply create a new class that implements the ILoanableItem interface.

public class Audiobook : ILoanableItem
{
    public double GetLoanPeriod()
    {
        return 21; // 21 days loan period for audiobooks
    }
}

How did we achieve OCP Here?

  1. Abstraction via Interfaces: The ILoanableItem interface abstracts the loan period logic, allowing different item types to provide their own implementation of GetLoanPeriod().
  2. Dependency on Abstractions: The LoanManager class depends on the ILoanableItem interface rather than concrete classes. This allows it to remain unchanged even as new item types are added.
  3. Extension via New Classes: New functionality (e.g., a new item type) is introduced by creating new classes that implement the ILoanableItem interface. These new classes do not require changes to existing code or classes, thus adhering to the principle.

By following this approach, the system becomes flexible, easy to maintain, and scalable, which are the primary goals of adhering to the Open/Closed Principle.

Summary

The Open/Closed Principle provides us with a powerful tool for designing software systems that are both flexible and maintainable. By organizing our code around abstractions and promoting the separation of concerns, we can build applications that can adapt quickly to changing business needs.


Similar Articles