In this article, we will be focusing on the Open/Closed Principle (OCP). OCP is the O in the SOLID principles, which are a set of software design principles. In the context of object-oriented programming and software development, these are intended to make software more understandable, flexible, and maintainable.

The acronym SOLID stands for:

  • SRP: Single-Responsibility Principle
  • OCP: Open/Closed Principle
  • LSP: Liskov Substitution Principle
  • ISP: Interface Segregation Principle
  • DIP: Dependency-Inversion Principle

The Open/Closed Principle (OCP)

OCP states:

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

Another way to interrupt this is:

When new changes arise, the existing code should allow new functionality to be added by creating new code that builds on the existing code. The existing code should not be directly altered to accommodate the new changes.

All systems change during their life cycles. This must be taken into consideration when developing systems expected to last longer than the first version. The OCP promotes code stability, reusability, and minimises the risk of introducing bugs, or breaking existing functionality when making changes to a system.

Description of OCP

Entities that conform to OCP have two primary attributes:

  1. They are open for extension. This means that the behaviour of that entity can be extended. As the requirements of the application change, we can extend the entity with new behaviours that satisfy those changes. In other words, we are able to change what the entity does.
  2. They are closed for modification. Extending the behaviour of a entity does not result in changes to the source code of the entity.

It would seem that these two attributes are at odds. The normal way to extend the behaviour of an entity is to make changes to the code of that entity. So how is it possible that the behaviours of an entity can be modified without changing its source code? Without changing the entity, how can we change what an entity does?

The answer is abstraction.

OCP Violation

Let’s look at the following code example of a DrawShape class.

public class DrawShape {
    private String type;
    public DrawShape(String type) {
        this.type = type;
    }
    public void draw() {
        if (type.equals("circle")) {
            drawCircle();
        } else if (type.equals("rectangle")) {
            drawRectangle();
        } else {
            throw new IllegalArgumentException("Invalid shape type: " + type);
        }
    }
    private void drawCircle() {
        // Code to draw a circle
    }
    private void drawRectangle() {
        // Code to draw a rectangle
    }
}

Can you think of how this code violates the OCP?

It’s pretty easy if we want to update this code, right? If we want to add the ability to draw a triangle then all we do is add a new if statement, check that the shape is a triangle, and call a drawTriangle() method. Easy!

The problem is, every time we want to add a new shape we have to come back and modify this code. So we violate the OCP.

Remember, code that conforms to the OCP is open for extension, but is closed for modification.

Code Refactor

So I said previously that abstraction solves this problem. To show you how, I’m now going to refactor the above code so that it conforms to the OCP.

public interface Shape {
    void draw();
}
public class Circle implements Shape {
    @Override
    public void draw() {
        // Code to draw a circle
    }
}
public class Rectangle implements Shape {
    @Override
    public void draw() {
        // Code to draw a rectangle
    }
}
public class ShapeDrawer {
    public void drawShape(Shape shape) {
        shape.draw();
    }
}

Instead, I have now written an interface called Shape with a single method named draw(). Both Circle and Rectangle are derivatives of the Shape interface.

Note that if we want to extend the behaviour of the drawShape() method in ShapeDrawer class to draw a new kind of shape, all we need to do is add a new derivative of the Shape interface. The drawShape() method does not need to change. Thus, ShapeDrawer now conforms to the OCP. It changes by adding new code (I.e. new classes such as Triangle, or Square, that implement the Shape interface) rather than by changing existing code.

Anticipation

Consider what would happen to the drawShape() method if we decided that we wanted to pass it a List of shapes, iterate over the list, and draw each shape. However, we want to make sure we draw all circles before we draw any other shape. The drawShape() method is clearly not closed to this kind of change. We would have to update the method parameter, scan the list first for circles, and draw them first.

Had we anticipated this change, we could have planned for it. But here’s the thing. We can’t plan for every single possible change. Our application would just end up being a wild bundle of abstractions.

Experienced designers hope that they know the users and the industry well enough to judge the probability of various kinds of changes, and invoke the OCP against the most probable changes. Also, conforming to the OCP is expensive. It takes time and effort to create appropriate abstractions, which also increases the complexity of the software design.

How do we know which changes are likely? We can do the appropriate research, ask the appropriate questions, and we can use our experience and common sense. And after all that, we wait until the changes happen!

Conclusion

In many ways, the OCP is at the heart of object-oriented design. Violating the OCP can lead to code that is difficult to maintain and prone to bugs. Code that conforms to the OCP is what yields the greatest results, and allows for the addition of new features or behaviours by introducing new code, rather than modifying existing code.

Rather than applying rampant abstraction to every part of the application, developers should only apply abstraction to those parts that exhibit frequent change. And after that, just wait for changes to happen. Resisting premature abstraction is as important as abstraction itself.

Scroll to Top