BrainsToBytes

The Open/Closed Principle

The Open Closed Principle is responsible for the 'O' of the S.O.L.I.D principles. Originally coined by Bertrand Meyer, it states that:

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

It seems like an incredibly simple principle, but it's behind most of the best practices we use when designing software. It's related to one of the main goals of software architecture: the proportion of changes in requirements and changes in the code. A small change in requirements should be accomplished without too much effort, while bigger features should still be possible to implement.

The principle has a single goal: to make the system easy to extend without impacting the code that already exists.

Why open? Why closed?

On the book Object-Oriented Software Construction(1988), by Bertrand Meyer, the following two definitions appear:

A module will be said to be open if it is still available for extension. For example, it should be possible to add fields to the data structures it contains, or new elements to the set of functions it performs.

A module will be said to be closed if it's available for use by other modules. This assumes that the module has been given a well-defined, stable description (the interface in the sense of information hiding)

We want modules that embody the two definitions:

  • We want modules to be open to extension. If requirements change and we get requests for new features, we'd like to accommodate them in our code. We want the ability to extend the behavior of our application so that we can adapt to our customer's needs.

  • We want modules to be closed for modifications, as every change has the potential to have negative side effects. If we create the right abstractions, we can depend mostly on well-defined and stable behaviors.

If a module is closed for modification and open for extension, we get the best of two worlds. Our code is both flexible and stable, letting our application grow in a controlled and structured way.

Those two ideas seem hard to reconcile, after all when we need to add a feature or perform a change, we usually open a class to add methods or modify already existing ones. This is exactly the behavior this principle stands against. Instead, we can add new behavior by writing new code, not by modifying the well-tested code that already works.

Closed reports, open reports

Imagine we have a Report class with some basic behavior and the ability to store its contents in a database using the save method.

class Report

    def initialize()
        #...
    end

    def add_info
        #...
    end

    def set_author
        #...
    end

    def genereate_footers
        #...
    end

    def save
        # Stores the report in a database
    end

end

As a new requirement, we need to be able to store the contents in new ways:

  • In a text file in the local machine.
  • In a network container.
  • In a cache (like Redis).

First solution attempt: More methods

We start to think about a good way to accommodate these changes and the firsts thing that comes to mind is creating new methods:

  • create a store_in_textfile method
  • create a store_in_network method
  • create a store_in_redis method

It works, but there's a problem: when management requests new ways of saving the reports, we will need to open the Report class and modify it. We will need to deal with the problems associated with violating the closed for modification clause of the OCP.

So, we need to find a way to extend the behavior without modifying Report every time a new requirement like this arises.

Second solution attempt: Use inheritance

We remember that we are using a class-based OO language and decide to use classical inheritance (classical as in class-based). We can inherit from Report and create subclasses based on the storage method:

OCPInheritance

This seems ok. After all, we don't need to open the original Report class to add new behavior to the application. All we need is to create a new subclass for every different storage requirement and we are done.

Still, inheritance might not be the perfect solution to this problem. You could argue that a ReportDB is-a type of Report, but semantically it doesn't make that much sense. What would happen if, for example, we wanted to create different types of report based on a different characteristic, something other than the way it gets stored?

Suppose we start subclassing new reports based on language and needed support for English, Spanish and Portuguese. Because they need different support for characters and other imaginary regulations, we would need the following classes:

Classes with support for the English language:

  • ReportDbEnglish
  • ReportTextfileEnglish
  • ReportNetworkEnglish
  • ReportRedisEnglish

Classes with support for the Spanish language:

  • ReportDbSpanish
  • ReportTextfileSpanish
  • ReportNetworkSpanish
  • ReportRedisSpanish

Classes with support for the Portuguese language:

  • ReportDbPortuguese
  • ReportTextfilePortuguese
  • ReportNetworkPortuguese
  • ReportRedisPortuguese

This exemplifies how easy it is to fall into the subclass-explosion trap down the road, even if the solution works well now. Let's take a look at another solution, one that respects the OCP.

Third solution: Isolate what changes

Currently, Report class has the knowledge needed to store information. After getting more and more requests for new ways of storing information, we know that this behavior is likely to change.

With this in mind, we can create a new abstraction for storing information, different classes will implement the concrete behavior for databases, textfiles and others. Those classes will be injected on Report class instances.

The Report class now has the following form:

class Report
    attr_accessor :information_saver
    def initialize(report_information_saver)
        @information_saver = report_information_saver
        #...
    end

    #...

    def save
        information_saver.save_info(self)
    end

end

report_information_server, as an abstraction, has only a save_info method that receives an object of type Report. The implementation details are left to the classes that implement this abstraction. In the end, you will have the following class hierarchy:

third_solution

This approach has several advantages:

  • The responsibility for storing information belongs into its own class, Report is now more aligned with the SRP.
  • We can add new behavior without needing to open any of the new classes, we just need a new subclass of report_information_server.
  • If we need new combinations of Report (like the languages example above), we just need to create classes for those characteristics and inject them, preventing class explosion.
  • We have a solution that will be easier to understand and maintain down the road.

Our third solution has a name: it's the Strategy Pattern. Most design patterns are actually ways of organizing your code so that the OCP principle is followed. We might study some of the most popular design patterns in future articles.

A principle at the heart of OOP

The Open Closed principle is one of the most important design principles. Following it enables you to use the best characteristics of OOP, and lets you create modular and easy-to-maintain applications. Using OO features like inheritance is not enough, you need to explicitly define the boundaries and abstractions in your application based on your requirements and design expertise.

In this article, we explored just one way of implementing the OCP principle, but there are countless other techniques at your disposal. As we mentioned before, design patterns are a popular way of ensuring your application is closed for modification and open for extension.

As a last thought, remember that it's impossible to make a completely closed program. In a real project, you can't protect your whole application from modification, it's bound to happen sooner or later.

What you can choose is what to close and what to leave open. The trick is to identify the things that are more likely to change and build the right abstractions to ensure we can add new behavior without modifying the code.

The quality of the boundaries you draw depend on your domain knowledge and design skills. It takes practice, so don't be afraid to build your own abstractions and see what works and what doesn't.

What to do next

  • Share this article with friends and colleagues. Thank you for helping me reach people who might find this information useful.
  • Read Uncle Bob's article on the OCP.
  • There's more info about the OCP in chapter 8 of Clean Architecture. This and other very helpful books are in the recommended reading list.
  • Send me an email with questions, comments or suggestions (it's in the About Me page). Come on, don't be shy!
Author image
Budapest, Hungary
Hey there, I'm Juan. A programmer currently living in Budapest. I believe in well-engineered solutions, clean code and sharing knowledge. Thanks for reading, I hope you find my articles useful!