🖋️
This article is one part of my series on Robert (Bob) C. Martin's SOLID principles.

The O in Bob's SOLID principles is in fact the brilliant Bertrand Meyer's open–closed principle, which is one of the five principles Meyer devised to define modularity.

Definition

The original definition, in Bertrand Meyer's 1988 book Object-Oriented Software Construction (from my 1997 second edition):

Modules should be both open and closed.

Which is as useful as defining air as airy and fire as fiery. But Meyer explains it further:

A module is said to be open if it is still available for extension. For example, it should be possible to expand its set of operations or add fields to its data structures.

This is clear. A module is open if we can extend what it can do in some way beyond what it already does.

A module is said to be closed if it is available for use by other modules. This assumes that the module has been given a well-defined, stable description (its interface in the sense of information hiding). At the implementation level, closure for a module also implies that you may compile it, perhaps store it in a library, and make it available for others (its clients) to use. In the case of a design or specification module, closing a module simply means having it approved by management, adding it to the project’s official repository of accepted software items (often called the project baseline), and publishing its interface for the benefit of other module authors.

This description is a little vague and shows its age somewhat. It indicates that the module has a clearly defined, stable public interface (API) with which other modules interact exclusively. This interface is reliable such that clients can expect it not to change.

I'm getting it, but let's see Bob's definition. He has taken this principle and plopped it into SOLID, after all, and that's what we're studying here.

In Clean Architecture, Bob quotes Meyer's book above (albeit the 1988 edition) in defining the term:

A software artifact should be open for extension but closed for modification.

I like this better than the definition I found, although I was unable to locate the source, despite checking out the first edition and reading the page 23 it is said to be from. I digress.

More from Bob:

In other words, the behavior of a software artifact ought to be extendible, without having to modify that artifact.

It looks like Bertrand and (Bob) Martin's definitions are related, but Bob's is clearer and more concise. We'll focus on Bob's here, since we are discussing his principles.

My British English spell checker isn't happy, but I think Bob has done a good job of defining this one.

The Elephant in the Room

The above definition makes sense to me, but with one big caveat: in a highly dynamic language like Ruby, which lets you shoot yourself in the foot whilst your colleagues hack it off at the knees, we need to pretend a little bit. So too in Python, Lua, and our other weird nemeses friends.

There is no such thing as "closed for modification" in Ruby. We can do practically anything, like completely re-define methods at runtime.

In places, we use the Data class for minimal boilerplate. See Lucian Ghinda's detailed post for more.
Person = Data.define(:first_name, :last_name) do
  def greet = puts "Hello, Mr. #{first_name} #{last_name}."
end

Person.new('Jamie', 'Schembri').greet
# => "Hello, Mr. Jamie schembri."

class Person
  def greet = puts "Hi #{first_name}."
end

Person.new('Jamie', 'Schembri').greet
# => "Hi Jamie."

Person.define_method(:greet) { puts "Bro."}

Person.new('Jamie', 'Schembri').greet
# => "Bro."

But just as we know we can do things like this — redefine methods, fetch instance variables (obj.instance_variable_get(:@first_name)), or call private methods (obj.send(:my_private_method)), we don't — bar some very special edge cases (read: hacks) and more meta requirements.

Technically, employing this dynamism does not modify what Bob has referred to as "the artefact," — the original class' code — but we do modify the artefact represented by that code — the class in memory.

What is public and accessible, and how it should behave, is part of the informal contract you implicitly sign when you make use of a module. So let's observe that contract and pretend that these unsupported methods of changing modules don't exist.

This means we need to build into our classes a supported method of extension, and a supported method of closure.

Example

Let's start with the example used above: that of a Person:

Person = Data.define(:first_name, :last_name) do
  def greet = puts "Hello, Mr. #{first_name} #{last_name}."
end

This module is de facto closed for modification; it is written and ready to use, and its public contract is that it responds to the greet method by printing a message.

Is it open to extension? Let's see...

Open Via Inheritance

It's open via inheritance!

class CasualPerson < Person
  def greet = puts "Yo #{first_name}!"
end

Since CasualPerson is a new module, it does not need to follow the same contract as Person. It could choose not to respond to greet at all, or have side-effects in doing so. The important thing is that this new contract — the CasualPerson contract — is abided by when using the CasualPerson class.

Inheritance has its own drawbacks (tight coupling, fragile base class, etc. but these are out of scope) — and it isn't a solution to every problem. What if, for example, we're working with an ORM which returns Person objects from queries? We might not be able to use CasualPerson in place of Person then.

Open via Dependency Injection

Rather than let the person itself decide what its greeting will look like, we can use Dependency Injection (DI) to "inject" our own behaviour. Let's update our Person class to handle this — we'll define the class more classically to take this into account.

class Person
  attr_reader :first_name, :last_name
  
  def initialize(first_name, last_name, greeter: DefaultGreeter)
    @first_name = first_name
    @last_name = last_name
    @greeter = greeter
  end

  def greet
    @greeter.greet(self)
  end
end

And here's what greeters might look like:

module DefaultGreeter
  def self.greet(person)
    puts "Hello, Mr. #{person.first_name} #{person.last_name}."
  end
end

module CasualGreeter
  def self.greet(person) = puts "Yo #{person.first_name}!"
end

module IllegalGreeter
  def self.greet(person) = raise "No greeting for you, #{person.first_name}!"
end

The interface of Person is the same as it was before, but now we can optionally pass in a different kind of greeter. The greeter need only respond to greet(Person), to which the person delegates the operation of greeting.

person = Person.new("Jamie", "Schembri", greeter: CasualGreeter)
person.greet
# => "Yo Jamie!"

The keen reader will recognise this this is an example of the Strategy pattern.

Other Techniques

There are a great deal of approaches to keeping a class open for extension and closed for modification. The key is in considering what is "hard-coded" in a class and determining whether it should be possible to override that in some way.

Some of the more common approaches you might run into:

  • The Decorator pattern: wraps an object to add/override behaviour on that object;
  • Configuration/Settings (via env, a file, etc.): behaviour can be changed with different settings; code needs to check these settings;
  • Callbacks: code injection at different points of execution.

Analysis

The open—closed principle aims to prevent us from modifying existing code by honouring its public interface as an unchanging contract. Every change we make can, after all, introduce new errors or cause other issues for our API consumers.

It's no surprise that OCP was developed in a time when Waterfall was the leading software development methodology. This isn't to say that it's inherently bad, but we need to be very careful in applying it with what we know about software development today, and taking with us a lot of context to inform our decisions.

Why?

The open—closed principle tries to account for change by covering every possible eventuality at the design phase, or at least allowing for extension such that any eventuality can be achieved. Then, when requirements inevitably come in requiring changes, we can make them without touching the existing Code That Must Not Be Changed.

On the flip-side, agile methodologies embrace change in a very different way. They also acknowledge that change can and will happen, but additionally acknowledge that the list of eventualities may also change — that it's impossible to determine all ways that code may ever need to change at the get-go. Instead, we should allow changes to have smaller impact by delivering smaller increments and tightening the feedback loop so that we can respond to change quicker.

If we rigidly follow OCP, we spend a lot more time designing modules that may never need extension — or may be dropped entirely — all because change may come. If change does come, it must be applied by way of new modules rather than the modification of existing ones.

Complexity skyrockets. Don't believe me?

This is the example Bob gives in Clean Architecture:

Imagine, for a moment, that we have a system that displays a financial summary on a web page. The data on the page is scrollable, and negative numbers are rendered in red.

Okay, that sounds pretty easy. What's his solution?

UML diagram of a financial reporting system with three main sections: Controller, Interactor, and Database. The Controller has two classes. The Interactor contains six classes and several interfaces for data structures and interactions. There are separate Screen Presenter and Print Presenter modules, each with three classes and interfaces. The Database section has two classes. Arrows indicate data and control flow between controllers, interactors, presenters, views, a data gateway, and the database.
This is what happens when you rigidly follow principles.

It speaks for itself.

Take-away

So what good can we take from OCP?

Consider your audience, and consider the impact of changing behaviour.

If you are building a module that only you and your small team will be using for the foreseeable future, please ignore OCP. It will lead to copious wasted time shipping things that nobody needs. See also: YAGNI.

Instead, focus on valuable functionality, and when you need your module to behave differently in certain cases, adopt a strategy based on your needs then. This tends to be my general strategy to software engineering, but everything needs context.

On the other end of the spectrum, let's say you're building a public-facing REST API for thousands of customers to consume. This is when OCP makes more sense: you almost never want to change such an API because people are building their apps relying on the way it works now. Making such a change might be very expensive, requiring changes across many applications and a deal of good communication.

Between these two extremes, it depends even more. Is your module used by several teams? Are you packaging and releasing something to the world? You might need to offer more opportunities for extension. All the more reason that dependencies aren't free.

To help, there are also ways of flagging behaviour that might change.

In a RESTful API, placing a warning in the documentation in addition to a sub-1.0 version number can communicate that it is subject to change. In code, we can use access modifiers (like public and private), allowing changes to code that should not be getting called externally — whether or not this is actually possible in your language doesn't matter as much as the intent. Conventions may also help, like RDoc's :nodoc: that removes a method from documentation, which the Rails team uses for internal methods and modules.

As with any principle, OCP is just another tool in the box. I don't think it is a tool that has aged well, but it still offers useful food for thought in some scenarios.