Open-closed Principle

Explore it with some examples!

Nicolas Nénon
ekino-france
6 min readJul 23, 2024

--

Image from Pixabay

This article was first inspired by a french talk about Open Closed principle by Thomas Dutrion at the Forum PHP 2023, see his talk on Youtube https://www.youtube.com/watch?v=g9fw5Zl4RF4.

In software engineering there is one acronym you will rapidly encounter: SOLID. What does SOLID mean? According to Wikipedia:

In software engineering, SOLID is a mnemonic acronym for five design principles intended to make object-oriented designs more understandable, flexible, and maintainable.

[…]

The SOLID ideas are:

The Single-responsibility principle: “There should never be more than one reason for a class to change.” In other words, every class should have only one responsibility.

The Open–closed principle: “Software entities … should be open for extension, but closed for modification.”

The Liskov substitution principle: “Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.”

The Interface segregation principle: “Clients should not be forced to depend upon interfaces that they do not use.”

The Dependency inversion principle: “Depend upon abstractions, [not] concretes.”

https://en.wikipedia.org/wiki/SOLID

There are five concepts behind the SOLID acronym, and in this article we will explore one of them: the open-closed principle (I will use the acronym OCP in the rest of the article). We will illustrate the principle with some PHP code examples.C

Let’s discuss the definition of the open-closed principle.

In object-oriented programming, the open–closed principle (OCP) states “software entities (classes, modules, functions, etc.) should be opened for extension, but closed for modification”; that is, such an entity can allow its behaviour to be extended without modifying its source code.

https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle

So according to the definition, each sofware entities must follow two rules:

  • Be closed to modification, i.e we don’t have to modify their code;
  • Be opened to extension, i.e we can add a new behavior (or functionality) without changing its code

The idea behind this principle is to reinforce the robustness of our code. By adding new behavior without changing the source code of the component (class, function), you minimize the risk to propagate bugs in your application.

As always, be mindful when following a principle, don’t follow it blindly, but try to understand the deeper idea behind it.

So, this is great, but how can we achieve OCP? Let’s explore some examples!

Using inheritance

One of the simplest (and old!) way to add new behavior. This mechanism is available in every OOP languages and with different flavors: multiple inheritance or single inheritance.

Here the simplest example:

class MyClass {

public function myCustomBehavior(){ }

}

class ExtendedClass extends MyClass {

public function myCustomBehavior(){
// option 1:
// we can call the original method
parent::myCustomBehavior()

// option 2:
// we can also do something before or after

// some code before
parent::myCustomBehavior()
// some code after

// option 3:
// we can omit to call the original method and do anything else here
// wich is the default setting
}

}

There are several issues using inheritance as we do. First, the open-closed principle can be violated if we use a public or protected member, as demonstrated in the following example:

class MyClass {

// public also works
protected int $increment;

public function incrementByOne(int $number){
return $number + $this->increment;
}

}

class ExtendedClass extends MyClass {

public function myExtendBehavior(){
$this->increment = -1;

// because we change the value of $increment
// the behavior of the function parent::incrementByOne
// has been changed.
// so OCP is broken
parent::incrementByOne();
}

}

This is why we always declare the member of a class private until we really need to set it to protected or public.

One another drawback of inheritance is that will quickly lead to a large hierarchy of classes, that will grow as large as the sum of combined behavior we need.

Using Composition

The OCP can be achieved using object composition. Composition is the way to combine objects into a more complex ones. This is a useful programming technique that can be used to apply the open-closed principle.

The first and simplest way to use composition is to use the Decorator pattern. By using it, we can define new behavior or redefine old one. A simple implementation is provided in the following example:

class MyClass {

public function myBehavior() {
// some useful code here
}

}

class MyDecoratedClass {

public function __construct(MyClass $objectToDecorate) { }

// wrap the call to $objectToDecorate
public function myBehavior() {
// some code before
$this->objectToDecorate->myBehavior();
// some code after
}

// add new method if we want
public function newBehavior() {

}
}

// example of usage
$myClass = new MyClass();

$decoratedClass = new MyDecoratedClass($myClass);

$decoratedClass->myBehavior(); // call override behavior
$decoratedClass->newBehavior(); // call new behavior

With the Decorator pattern, we can modify the decorated object without changing its code. Nowadays, every major framework (like Symfony or Laravel) provides a mechanism to make decoration more configurable by using YAML, XML or PHP configuration.

Another pattern can be used too. This is the observer (or publish / subscribe) pattern. The pattern defines one-to-many dependency where states changes are notified to a group of objects. Here is a primitive implementation of it:

// each class that want to be notified
// need to implements this interface
interface Observer {
// here Event class is a kind of DTO
// that contains useful data
public function on(Event $event);
}

public function MyClass {
private array $observers;

public function addObserver(Observer $observer): void{
// simple way to add an new observer
// IRL we may need to check if the observer has been already added
$this->observers[] = $observer;
}

public function myBehavior(): void{
// do something here

$event = new Event(/* data */);

// notify observer that something happens
$this->notify($event);
}

private function notify(Event $event): void{
foreach($this->observers as $observer){
$observer->on($event);
}
}
}

// new class to add new behavior when MyClass do something
class MyNewClass implements Observer {
public function on(Event $event){
// something happens, we can add our logic here
}
}

// and in the code base
$myClass = new MyClass();
$myNewClass = new MyNewClass();

$myClass->addObserver($myNewClass);

$myClass->myBehavior(); // here $myNewClass->on() will be called

We can add more complexity by adding the notion of post/pre event. Alternatively, we can use an object whose responsibility is to only handle event messages.

This kinds of objects are called bus message and the design pattern becomes publisher-subscriber pattern, because objects create events (the publishers) and others consume them (the subscribers).

Each major PHP framework offers a powerful mechanism for this. Names can vary but Event and Event Listener are widely use nowadays.

There are more techniques to apply the OCP.

One example is the Entity Component System (ECS), mainly used in video game development (see Unity as a direct implementation). This pattern allows the developer to add new “functionality” to an entity by aggregating other components (such as physics, 2D display).

ECS can be applied to other domains as well, even if it is not widely used outside the video game development world.

We can also talk about old techniques which are still relevant, for adding or changing the behavior of a piece of code. For instance, callback functions are functions passed to an object (or method) and then call to execute custom behavior. We can think about them as a primitive observer pattern. Here an example illustrating the callback function mechanism:

class MyClass {
public function myBehavior(callable $callback): void
{
// do something here
// build $event with some useful data for the callback
call_user_func($callback, $event)
}
}

function myCallBack($event): void
{
// do something with $event here
}

$myObject = new MyClass();
$myObject->myBehavior('myCallBack');

Callback functions were used intensely in javaScript world, as event-driven programming tended to use them for asynchronous programming. Nowadays there are other techniques like Promises, async functions, generator mechanisms to avoid the infamous callback hell.

Conclusion

The Open-closed principle leads the code that is more robust to change and less prone to bugs.

Robustness is an important property of code, as it ensures greater stability over time and reduces the likelihood of bugs. You can explore the robustness principle on Wikipedia as a starting point.

The famous SOLID acronym should be seen as one comprehensive principle where each “letter” is not independent from the others, but complementary with each other.

This means that if we want to apply the “O” letter, we also need to understand (and apply) other principles represented by “S,” “L,” “I,” and “D” (Single responsability, Liskov substitution, Interface segregation and Dependency inversion).

Applying this open-closed principle blindly can lead to multiple small classes, resulting in over-engineering and a system that is not easy to maintain.

One way to avoid this is to explore and understand other principles, such as:

  • DRY Don’t Repeat Yourself
  • CUPID principles (Composable, Unix Philosophy, Predictable, Idiomatic, Domain Based)
  • KISS Keep It Simple, Stupid!
  • YAGNI You Aren’t Gonna Need It
  • GRASP General Responsibility Assignment Software Patterns

And don’t be afraid to mix them! Each principle can work together and isn’t exclusive.

--

--