Do you REALLY know what SOLID principles means? (#2: Exclusive Truth about Open Closed Principle)

Introduction

The Open/Closed Principle (OCP) is a core concept in object-oriented design, stating that software entities—such as classes, modules, and functions—should be “open for extension but closed for modification.” Introduced by Bertrand Meyer in 1988 and later popularized by Robert C. Martin in the early 2000s as part of the SOLID principles, the OCP emphasizes the need for software systems to accommodate new functionalities without altering existing code, thereby enhancing maintainability and minimizing the risk of introducing bugs.[1][2][3] This principle has gained prominence in modern software development practices, particularly within Agile methodologies, where frequent updates are essential to adapt to evolving user requirements and business needs.[4][5]

Implementing the OCP typically involves the use of abstraction techniques, such as interfaces and inheritance, which allow developers to introduce new features as separate entities that interact with existing code without direct modifications.[6][7][8] For instance, in a plugin-based web browser, new functionalities can be integrated seamlessly through extensions, exemplifying how systems can remain flexible and

maintainable while adhering to the OCP.[8] However, developers must apply this prin- ciple judiciously; strict adherence can lead to over-engineered systems characterized by excessive complexity and unnecessary abstractions, complicating the codebase and potentially hindering development efforts.[1][9]

Despite its benefits, the OCP also presents challenges, including design complexity and the difficulty of anticipating future changes during the software development process. This can lead to a “subclass explosion,” where numerous subclasses

are created to handle varying behaviors, making the codebase cumbersome and

challenging to navigate.[10][11] Additionally, the introduction of extensive abstrac- tions may complicate testing and debugging efforts, creating potential pitfalls for developers who strive for a balance between extensibility and maintainability.[10][12]

Overall, the Open/Closed Principle serves as a foundational guideline for creating robust and adaptable software systems. By fostering a design philosophy that pri- oritizes flexibility and maintainability, the OCP remains a critical consideration for developers aiming to navigate the complexities of modern software engineering while delivering high-quality applications.[2][13][5]

Open for Extension, Closed for Modification

The Open Closed Principle is a popular software design principle that states that classes should be open for extension, but closed for modification. This means that we should be able to add new functionality to a class without modifying the existing code for that class. The reason for this is that modifying existing code can introduce potential bugs and risks into our system.

Classes should be open for extension, but closed for modification. By doing so, we stop ourselves from modifying existing code and causing potential new bugs. What Open Close Principle wants to say is – We should be able to add new functionality without touching the existing code for the class. This is because whenever we modify the existing code, we are taking the risk of creating potential bugs.

So we should avoid touching the tested and reliable (mostly) production code if possible. But how are we going to add new functionality without touching the class? It is usually done with the help of interfaces and abstract classes.
We can make sure that our code is compliant with the open/closed principle by utilising inheritance and/or implementing interfaces that enable classes to polymorphically substitute for each other.

Interface and Abstract Class

To add new functionality without modifying a class, we can utilize interfaces and abstract classes. These allow us to add new functionality through inheritance or implementation, without changing the existing code for a class. This allows us to add new features to our software without affecting the existing codebase.

In practice, we need to consider how the requirements of our software are likely to change in the future. We should determine what should be left abstract for our module’s consumers to make concrete, and what concrete functionality we should provide. It’s important to find a balance between being partially concrete (to do something useful) and partially abstract (to be used in a variety of contexts).

Consider the following example:

class Post
{
    void CreatePost(Database db, string postMessage)
    {
        if (postMessage.StartsWith("#"))
        {
            db.AddAsTag(postMessage);
        }
        else
        {
            db.Add(postMessage);
        }
    }
}

This implementation violates the open close principle because it differs the behavior based on the starting letter of the postMessage. If we later wanted to also include mentions starting with ‘@’, we’d have to modify the class with an extra ‘else if’ in the CreatePost() method.

To make this code compliant with the open close principle, we can use inheritance as follows:

class Post
{
    void CreatePost(Database db, string postMessage)
    {
        db.Add(postMessage);
    }
}

class TagPost : Post
{
    override void CreatePost(Database db, string postMessage)
    {
        db.AddAsTag(postMessage);
    }
}

By using inheritance, it is now much easier to create extended behaviour to the Post object by overriding the CreatePost() method. The evaluation of the first character ‘#’ will now be handled elsewhere of our software, and if we want to change the way a postMessage is evaluated, we can change the code there, without affecting any of these underlying pieces of behaviour.

The general idea of this principle is great.

It tells us to write our code so that we will be able to add new functionality without changing the existing code. That prevents situations in which a change to one of our classes also requires us to adapt all depending classes. Unfortunately, many of the tutorial out there proposes (the example given above is one such case; sadly) to use inheritance to achieve this goal.
But more often than not; inheritance introduces tight coupling if the sub-classes depend on implementation details of their parent class.

That’s why we need to redefine the Open/Closed Principle to the Polymorphic Open/Closed Principle. It uses interfaces instead of super-classes to allow different implementations which you can easily substitute without changing the code that uses them. The interfaces are closed for modifications, and we can provide new implementations to extend the functionality of your software.
The main benefit of this approach is that an interface introduces an additional level of abstraction which enables loose coupling. The implementations of an interface are independent of each other and don’t need to share any code.

Open Closed Principle

Tight Coupling

However, it’s important to note that inheritance can sometimes introduce tight coupling between classes, as the subclass may depend on implementation details of the parent class. To avoid this issue, we can utilize the Polymorphic Open Close Principle, which uses interfaces instead of inheritance to enable different implementations to be easily substituted without changing the code that uses them. This approach introduces an additional level of abstraction and allows for loose coupling between implementations, as they are independent of each other and don’t share any code.

An interesting observation is – the word extend doesn’t necessarily mean that we should subclass the actual class that needs the new behaviour. Let’s consider an example:

public class Context {
    private IBehavior behavior;

    public void doStuff() {
        if (this.behavior != null)
            this.behavior.doStuff();
    }

    public void setBehavior(IBehavior behavior) {
        this.behavior = behavior;
    }
}

public interface IBehavior {
    public void doStuff();
}

..............................

public class HelloWorldBehavior implements IBehavior {
    public void doStuff() {
        System.println("Hello world!");
    }
}

public class GoodByeBehavior implements IBehavior {
    public void doStuff() {
        System.out.println("Good bye cruel world!");
    }
}

...................................

// in main method
Context c = new Context();

c.setBehavior(new HelloWorldBehavior());
c.doStuff();
// prints out "Hello world!"

c.setBehavior(new GoodByeBehavior());
c.doStuff();
// prints out "Good bye cruel world!"

In the example above the Context is locked for further modifications.
Most programmers would probably want to subclass the class in order to extend it but here we don’t because it assumes it’s behaviour can be changed through anything that implements the IBehavior interface. Using this pattern we can modify the behaviour of the context at runtime, through the setBehavior method as extension point.
So whenever we want to extend the “closed” context class, let’s try do it by sub-classing it’s “open” collaborating dependency.

OCP: How to Follow

just like the SRP, the way we follow this principle in practice is determined by an educated guess about how the requirements on our software are likely to change in future. In the SRP we make a judgement about decomposition and where to draw encapsulation boundaries in our code. In the OCP, we make a judgement about what in our module we will make abstract and leave to our module’s consumers to make concrete, and what concrete functionality to provide by us. Any sensible module must lie in between these extremes: to do something useful it has to be partially concrete, however to be used in a range of contexts it must be partially abstract.

Violating one principle but following the other: Follows OCP but not LSP

Lets say we have the given code:
public interface IPerson {}

public class Boss implements IPerson {
    public void doBossStuff() { ... }
}

public class Peon implements IPerson {
    public void doPeonStuff() { ... }
}

public class Context {
    public Collection<IPerson> getPersons() { ... }
}

This piece of code follows the open-closed principle. If we’re calling the context’s GetPersons method, we’ll get a bunch of persons all with their own implementations. That means that IPerson is closed for modification, but open for extension.
However things take a dark turn when we have to use it:

// in some routine that needs to do stuff with 
// a collection of IPerson:
Collection<IPerson> persons = context.getPersons();
for (IPerson person : persons) {
    // now we have to check the type... :-P
    if (person instanceof Boss) {
        ((Boss) person).doBossStuff();
    }
    else if (person instanceof Peon) {
        ((Peon) person).doPeonStuff();
    }
}

We have to do type checking and type conversion which violates LSP! We can get out of this situation by either doing some pull-up refactoring or implementing a Visitor pattern.
In this case we can simply do a pull up refactoring after adding a general method:

public class Boss implements IPerson {
    // we're adding this general method
    public void doStuff() {
        // that does the call instead
        this.doBossStuff();
    }
    public void doBossStuff() { ... }
}


public interface IPerson {
    // pulled up method from Boss
    public void doStuff();
}

// do the same for Peon

The benefit now is that we don’t need to know the exact type anymore, following LSP:

// in some routine that needs to do stuff with 
// a collection of IPerson:
Collection<IPerson> persons = context.getPersons();
for (IPerson person : persons) {
    // yay, no type checking!
    person.doStuff();
}

Overview

The Open/Closed Principle (OCP) is a fundamental tenet of object-oriented design that asserts that software entities such as classes, modules, and functions should be “open for extension, but closed for modification”[1][2]. This principle encourages developers to design systems that allow new functionalities to be added without altering existing code, thereby enhancing maintainability and reducing the risk of introducing bugs[14][2]. By adhering to this principle, developers can create robust and scalable software that accommodates evolving user requirements and business needs.

To effectively implement the Open/Closed Principle, developers often utilize ab- straction through interfaces or abstract classes. This enables the creation of new functionalities as separate entities, which can interact with the existing system without direct modification[6][7]. For example, when extending a feature within a software application, developers can introduce new behaviors or properties that build on the existing functionality while ensuring that the core logic remains unchanged[14][15].

However, it is crucial to apply the Open/Closed Principle judiciously, as blind ad- herence may lead to an over-engineered system characterized by excessive small classes and convoluted structures[1]. To prevent such issues, developers are en- couraged to integrate other principles of software design, such as the Don’t Repeat Yourself (DRY) principle, which advocates for reducing redundancy within code[- 1][15]. Ultimately, the Open/Closed Principle plays a vital role in fostering flexible and maintainable software systems, making it an essential consideration for modern software development practices[2][16].

Historical Context

The Open-Closed Principle (OCP) is one of the five foundational principles of ob- ject-oriented design encapsulated by the acronym SOLID, which was popularized by Robert C. Martin, also known as Uncle Bob, in the early 2000s.[13][4]. The OCP states that software entities such as classes, modules, and functions should be “open for extension but closed for modification,” a concept originally introduced by Bertrand Meyer in 1988[3]. This principle emphasizes the importance of designing software in a way that allows it to be extended without altering its existing codebase, thereby enhancing maintainability and reducing the risk of introducing bugs.

Historically, the need for the Open-Closed Principle arose as software systems grew increasingly complex and required frequent updates and enhancements. The traditional approach of modifying existing code often led to issues such as regression bugs, where changes would inadvertently disrupt the functionality of previously work- ing features. OCP was proposed as a solution to mitigate these risks, encouraging developers to use polymorphism and abstraction to create flexible systems that could accommodate new requirements without the need for direct modification of existing code[17][4].

As software development practices evolved, the significance of the OCP became more pronounced, particularly in the context of Agile and Adaptive software method- ologies. These methodologies advocate for iterative development and frequent changes, further underscoring the importance of principles like OCP to ensure

that the underlying code remains robust and adaptable[4]. The adoption of the Open-Closed Principle has influenced numerous design patterns, including the Strategy pattern, which exemplifies how to implement extensibility without modifying existing code[3].

Today, the Open-Closed Principle continues to serve as a guiding tenet for developers seeking to create high-quality, maintainable, and adaptable software systems in an ever-changing technological landscape[13][5].

Key Concepts

The Open/Closed Principle (OCP) is a fundamental concept in object-oriented design that promotes software entities being open for extension but closed for modification- [18][19]. This principle encourages developers to structure their code in a way that allows new functionality to be added without altering existing code, thereby reducing the risk of introducing bugs and ensuring greater maintainability[5][20].

Definition and Importance

The essence of the Open/Closed Principle is to ensure that once a class or module has been developed, it should be possible to extend its capabilities without modifying its source code[5]. This approach is crucial in scenarios where changes to existing code could lead to regressions or necessitate alterations across multiple components of the software[8]. By adhering to OCP, developers can create systems that are both flexible and resilient to change, enabling new features to be integrated seamlessly.

Practical Application

In practice, the OCP can be achieved through techniques such as inheritance and interfaces. For example, in a web browser that supports plugins, new features can be added through extensions without modifying the core browser functionality[8]. This model exemplifies how software can be designed to be “closed” to modification yet “open” to extension, allowing for a dynamic and adaptable architecture that meets evolving requirements[19].

Benefits of the Open/Closed Principle

The implementation of OCP offers several benefits, including:

Increased Flexibility: The ability to add new features without affecting the existing codebase allows for rapid development and iteration[8].

Reduced Risk of Bugs: As the original code remains unchanged, the likelihood of introducing new errors is minimized[5][20].

Enhanced Maintainability: Systems designed with OCP in mind tend to be easier to maintain, as new requirements can be accommodated without significant overhaul of the existing code structure[19].

By embracing the Open/Closed Principle, developers can foster a design philosophy that prioritizes longevity and adaptability in software development.

Implementation

Overview of the Open-Closed Principle

The Open-Closed Principle (OCP) is a fundamental concept in software engineering that asserts that software entities such as classes, modules, and functions should be open for extension but closed for modification. This principle encourages developers to write code that can be easily extended with new functionalities without altering existing code, thereby reducing the risk of introducing bugs and enhancing main- tainability. The implementation of OCP can be achieved through various techniques, including inheritance, interfaces, composition, and dependency injection[1][21].

Using Inheritance

One of the most straightforward ways to implement OCP is through inheritance. This method allows developers to create new classes that extend the functionality of existing ones without modifying the original class. For example, consider a base class with a method . By creating a subclass , developers can either override this method to introduce new behavior or extend it while still retaining access to the base functionality via [21].

Example of Inheritance

Interfaces and Composition

In addition to inheritance, the OCP can also be effectively implemented using interfaces and composition. By defining clear interfaces, developers can create various implementations that adhere to those interfaces without changing the existing codebase. This allows for new functionalities to be integrated seamlessly, promoting code modularity and reducing complexity[1].

Example of Interfaces

In this example, both and classes can be introduced without modifying the existing interface, demonstrating the extensibility of the design.

Real-World Use Case

To illustrate the effectiveness of the OCP, consider an e-commerce platform that re- quires multiple payment methods. Initially, a component handles credit card paymen- ts. When additional payment methods are needed, such as PayPal or cryptocurrency, developers can create new classes implementing the interface, allowing the checkout process to expand without altering the core functionality of the . This approach ensures that the system remains flexible and maintainable as new requirements emerge[1].

Benefits of OCP Implementation

Implementing the Open-Closed Principle yields several advantages:

Quality: By adhering to OCP, the design and architecture of the codebase improve, resulting in a more reliable and maintainable system.

Usability: New features can be added easily, enhancing the user experience without disrupting existing functionalities.

Maintainability: Developers can extend systems with minimal risk of unintended consequences, making updates easier over time.

Code Reusability: Components designed with OCP in mind can be reused across different parts of the application, minimizing duplication.

Scalability: OCP allows the application to grow and adapt to new requirements without extensive modifications to the existing code[21].

By embracing the Open-Closed Principle, developers can create robust, flexible, and scalable software solutions that adapt to changing needs while preserving the integrity of existing code.

Examples

Real-World Applications of the Open-Closed Principle

Stock Trading Application

In a hypothetical stock trading application, a new parent class named can be introduced, which includes a method. Both and classes inherit from this class, allowing them to share common functionality while remaining closed for modification. This design adheres to the Open-Closed Principle by enabling the addition of new transaction types without altering the existing transaction classes[22].

Payment Processing Systems

A practical example of the Open-Closed Principle is found in payment processing systems. Here, a base class can include methods for processing payments, while subclasses such as and introduce new payment methods without modifying the existing payment processing logic. This structure fosters adaptability in a dynamic business environment, allowing new payment options to be seamlessly integrated as needed[9].

E-Commerce Application

In an e-commerce application initially designed to sell books and electronics, devel- opers may face the challenge of adding fashion items as the business grows. Instead of modifying existing classes or duplicating code, which carries the risk of introducing bugs, the Open-Closed Principle allows developers to create a new class for fashion items that extends the base product class or implements the necessary interfaces. This method ensures that the original codebase remains intact while accommodating new functionality[23].

Design Patterns Supporting the Open-Closed Principle

Strategy Pattern

The Strategy Pattern exemplifies the Open-Closed Principle by allowing the definition of a family of algorithms, encapsulating each one, and making them interchangeable. This pattern enables behaviors to be selected at runtime without altering the classes that use them, thus promoting flexibility and maintainability in software design. For instance, in a project requiring connection type caching, different caching strategies can be implemented based on connection conditions without modifying the existing cache handling code[24].

Decorator Pattern

The Decorator Pattern aligns closely with the Open-Closed Principle by allowing additional behavior to be added to individual objects dynamically, without changing their underlying classes. This approach supports the principle’s goal of extending functionality without altering existing code, making it particularly useful in scenarios where enhancing object behavior is necessary without disturbing the established class hierarchy[25].

Factory Pattern

Lastly, the Factory Pattern fosters compliance with the Open-Closed Principle by managing the creation of objects without exposing instantiation logic. This pattern allows for the introduction of new concrete classes while safeguarding existing workflows, thereby facilitating extensions and adaptability in software applications. An

example in a NestJS application could involve creating different types of notifications (such as email, SMS, etc.) without altering the notification service’s core logic[26].

Challenges and Limitations

Implementing the Open-Closed Principle (OCP) presents several challenges for developers, primarily related to balancing extensibility and maintainability. Striving to make a system open for extension may lead to increased complexity, complicating future enhancements and resulting in a convoluted codebase[9].

Design Complexity

One significant limitation of the OCP is the increase in design complexity. Adhering to this principle often necessitates the use of abstractions, such as abstract classes and interfaces, which encapsulate common behaviors for future extension. While these abstractions can enhance modularity, they can also make the codebase more challenging to understand and maintain. Team members might spend excessive time deciphering intricate structures rather than focusing on functionality, thus raising questions about the necessity of these abstractions[10][12].

Anticipating Future Changes

A further challenge arises from the need to anticipate potential future changes during the design phase. Developers are often required to predict all possible modifications that may occur in the system, which is impractical in real-world development sce- narios. This can lead to extended design phases, consuming additional time and resources as developers attempt to foresee every possible outcome[11][10].

Reusability vs Complexity

While the OCP aims to increase reusability, it may inadvertently introduce additional overhead to the codebase. The creation of new classes or modules to comply with the OCP can impact system performance and slow down the development process, as developers need to manage a larger number of components[10].

Testing and Debugging

The use of abstractions and design patterns also complicates testing and debugging. The presence of dependencies across various layers can make it challenging to identify and resolve issues. Developers may find it difficult to write effective unit tests or trace bugs in a complex hierarchy of components, which can hinder the overall development workflow[10][12].

Related Principles

The Open/Closed Principle (OCP) is one of the five SOLID principles that guide software design and development, and it is inherently interconnected with the other

principles in the SOLID acronym: Single Responsibility, Liskov Substitution, Interface Segregation, and Dependency Inversion. Understanding these relationships can significantly enhance the robustness and maintainability of software systems.

Single Responsibility Principle (SRP)

The Single Responsibility Principle states that a class should have only one reason to change, meaning it should only have one responsibility or job. This principle com- plements the OCP by ensuring that when features are extended, they do not intro- duce additional responsibilities to existing classes, thus adhering to the open/closed concept where classes are open for extension but closed for modification[14][27]. For example, if a class is solely responsible for one feature, it can be extended with new behaviors without altering its core functionality.

Liskov Substitution Principle (LSP)

The Liskov Substitution Principle asserts that objects of a superclass should be re- placeable with objects of a subclass without affecting the correctness of the program. This principle ensures that when implementing the OCP, subclasses can extend functionality in a way that is consistent with their parent class[27][28]. When following OCP, it is crucial that any new class introduced does not violate the expectations set by the superclass, thus maintaining the integrity of the system’s design.

Interface Segregation Principle (ISP)

The Interface Segregation Principle advocates for creating small, client-specific interfaces rather than a single general-purpose interface. This principle supports the OCP by allowing classes to implement only the methods that are relevant to them, thus facilitating easier extensions[27][1]. When new functionalities are required, they can be added through new interfaces without forcing existing classes to implement unused methods, thereby adhering to the open/closed philosophy.

Dependency Inversion Principle (DIP)

The Dependency Inversion Principle states that high-level modules should not de- pend on low-level modules but rather on abstractions. This principle aligns closely with the OCP by promoting the idea that dependencies should be managed through interfaces or abstract classes, enabling easier modifications and extensions[27][28]. When a new feature is added, it can rely on abstractions, ensuring that existing code remains unchanged and thus open for extension.

Further Reading

Overview of the Open Closed Principle

The Open Closed Principle (OCP), articulated by Bertrand Meyer in 1988, empha- sizes that software entities such as classes, modules, and functions should be “open for extension but closed for modification”[5]. This principle is a cornerstone of Ob- ject-Oriented Programming (OOP) and aims to enhance software maintainability and flexibility. By adhering to OCP, developers can create systems that accommodate new functionalities without necessitating alterations to existing code, thereby reducing the risk of introducing bugs.

Design Patterns and OCP

A practical application of the Open Closed Principle is through the use of design patterns, particularly the Strategy Pattern. This pattern allows for the encapsulation of varying algorithms or behaviors within separate classes, which can then be injected as dependencies into other classes. For instance, a class can utilize different strategies (such as saving to a database or a text file) without modifying its core logic[15]. This demonstrates how design patterns help uphold the OCP by fostering a modular architecture that promotes easy extensions.

Challenges in Implementing OCP

Despite its advantages, the Open Closed Principle can present challenges. Develop- ers often encounter the “subclass-explosion” problem when trying to implement OCP by creating numerous subclasses for different variations of behavior[15]. Additionally, many software systems do not inherently support separate extensions, which can lead to “shotgun surgery,” where changes necessitate widespread modifications across codebases[29]. Tools like Eclipse or Visual Studio exemplify environments that support OCP through the use of plugins, which can extend functionalities without altering core code[29].

The Role of SOLID Principles

The Open Closed Principle is one of the five SOLID principles, which provide guidelines for designing robust and maintainable software. Understanding these principles is crucial for developers aiming to enhance the design and architecture of their applications[28]. The integration of OCP with other SOLID principles—such as the Single Responsibility Principle and the Dependency Inversion Principle—can lead to cleaner, more testable code[28][30].

Further Exploration

For those interested in delving deeper into the Open Closed Principle and its applications in modern software development, several resources are available. Key readings include Bertrand Meyer’s foundational work, “Object-Oriented Software Construction,” which explores the theoretical underpinnings of OCP[31]. Additionally, Robert C. Martin’s article, “The Open-Closed Principle,” provides practical insights into applying this principle in software design[32]. Exploring these resources can en-

rich understanding and facilitate better implementation of the Open Closed Principle in various programming contexts.

Conclusion

Open close principle is about locking the working code down but still keeping it open somehow with some kind of extension points. This is to avoid code duplication by encapsulating the code that changes. It also allows for failing fast as breaking changes are painful (i.e. change one place, break it everywhere else).
For the sake of maintenance the concept of encapsulating change is a good thing, because changes always happen. The Open Close Principle is a valuable design principle that helps us add new functionality to our software without modifying existing code. By utilizing interfaces and abstract classes, we can follow the Polymorphic Open Close Principle and create flexible and decoupled design.

Leave a Comment