Open/Closed Principle (OCP): Designing for Extensibility Over Modification

open closed principle solid banner overcoded

The Open/Closed Principle (OCP) is the second principle of SOLID object-oriented design patterns outlined by Robert C. Martin (a.k.a. Uncle Bob) in his 2002 Agile Software Development, Principles, Patterns, and Practices. It states that software entities should be “open for extension but closed for modification.” (1)

The Open/Closed Principle provides a conceptual basis for maintaining flexibility as software evolves and grows. This means focusing on extensibility over modification. More concretely, extensible software entities are designed such that additional functionality can be added without requiring modification to other interrelated entities.

Non-Breaking Changes

As with many design principles, the OCP has a strong focus on abstraction. Particularly, the OCP is often achieved by using abstract base classes to factor out implementation specifics. Common code smells like identically-named methods among multiple classes often signify opportunities where implementing the OCP may be beneficial.

This approach allows extended functionality via concrete implementation classes without necessitating changes to base classes. In other words, a developer can add new functionality without changing the existing code of related functions, classes, and methods!

Example of OCP Violation

To exemplify the OCP, let us again consider a hypothetical stock trading application, continuing from the examples in the article about the Single Responsibility Principle (SRP) of SOLID design.

Previously, this example provided a single Buy and Sell as part of its Transaction class. This time, we’ll incorporate functionality to make a record of Buy and Sell transactions. After all, one needs something to show come tax season!

class Transaction {
    
    private void buy(String ticker, int quantity, float price){
        Buy.execute(ticker, quantity, price);
    }
    private void sell(String ticker, int quantity, float price){
        Sell.execute(ticker, quantity, price);
    }
}

class Buy{

    // Create a record of the Buy transaction
    public static String makeRecord(String ticker, int quantity, float price, String date){

        // Create a string containing relevant Buy metadata
        return MessageFormat.format("{0}-{1}-{2}-{3}",
                ticker, String.valueOf(quantity),String.valueOf(price),date);
    }

    // Class-specific Buy logic
    static void execute(String stock, int quantity, float price){
        // Execute buy action here
    }

}

class Sell{

    // Create a record of the Buy transaction
    public static String makeRecord(String ticker, int quantity, float price, String date){

        // Create a string containing relevant Buy metadata
        return MessageFormat.format("{0}-{1}-{2}-{3}",
                ticker, String.valueOf(quantity),String.valueOf(price),date);
    }

    // Class-specific Sell logic
    static void execute(String stock, int quantity, float price){
        // Execute buy action here
    }
}

Notice that the Buy and Sell classes each implement their own version of the makeRecord method. My nose recoils when copy/pasting blocks of code such as this, signifying potential code smell.

The design of this code violates the Open/Closed principle because anytime a record format needs to be adjusted it requires a change in both the Buy and Sell class. In other words, a single extension requires multiple modifications.

Example of OCP Adherence

One possible approach to approving the above code, with the Open/Closed Principle in mind, is to use inheritance. This dictates that child classes, inheriting from a parent class, should share similar functionality.

This approach allows the Buy and Sell methods to share a common method to create records of transactions. This allows the functionality of making records to be extended without requiring modification of either Buy or Sell. They just call the inherited makeRecord method!

class RecordableTransaction {

    // Create a record, in a format used by all inherited classes
    public static String makeRecord(String ticker, int quantity, float price, String date){

        // Create a string containing relevant Buy metadata
        return MessageFormat.format("{0}-{1}-{2}-{3}",
                ticker, String.valueOf(quantity),String.valueOf(price),date);
    }
}

class Transaction {
    
    // Still uses Buy class to execute buy transactions
    private void buy(String ticker, int quantity, float price){
        Buy.execute(ticker, quantity, price);
    }

    // Still uses the Sell class to execute sell transactions
    private void sell(String ticker, int quantity, float price){
        Sell.execute(ticker, quantity, price);
    }
}

class Buy extends RecordableTransaction{

    // Class-specific Buy logic still implemented here
    static void execute(String stock, int quantity, float price){
        // Execute buy action here
    }

}

class Sell extends RecordableTransaction{

    // Class-specific Sell logic still implemented here
    static void execute(String stock, int quantity, float price){
        // Execute sell action here
    }
}

This version approaches the design by using a new Parent class named RecordableTransaction that defines a makeRecord method. The Buy and Sell classes, now children of the RecordableTransaction class, both share the makeRecord method and its implementation. The Open/Closed Principle UML diagram example below provides a visual of what is going on here:

open closed principle uml diagram example overcoded
Inheriting from the RecordableTransaction class allows extension of the makeRecord method without requiring modification among any child classes such as Buy or Sell

This is one of many approaches to abstract common functionality in the hypothetical stock trading application. For another example, check out Joel Abrahamsson’s implementation of the Open/Closed Principle’s prototypical Shape example.

Final Thoughts

The SOLID framework for better object-oriented design has stood apart from other more transient models throughout the years. Its AGILE approach to designing better code helps avoid common mistakes that prevent software from being flexible, easily maintainable, and scalable.

The Open/Closed Principle is but one of the five class/module principles. Its guidance for class-based designs focused on ease-of-extensibility is revered among engineers, programmers, and hobbyist coders alike. Yes—you have to put on your thinking cap initially to understand its conceptual approach. Yes—it takes more time to implement. And Yes—sometimes the extra time is warranted for small-scale applications or rapid-prototyping.

As a closing thought, I will argue that even when implementing the OCP is not warranted one will still benefit from being aware of its potential use in development.

References

  1. Martin, Robert C. Agile Software Development, Principles, Patterns, and Practices. 1st ed., Pearson, 2002.
Zαck West
Full-Stack Software Engineer with 10+ years of experience. Expertise in developing distributed systems, implementing object-oriented models with a focus on semantic clarity, driving development with TDD, enhancing interfaces through thoughtful visual design, and developing deep learning agents.