OCP: The Open-Closed Principle

Write reusable and scalable code

Abu Jobaer

--

Note: This publication demonstrates the Open-Closed principle in the following languages: TypeScript and PHP. See full code examples: TypeScript and PHP.

OCP stands for Open-Closed Principle. It is one of the principles of Object-Oriented Design. Robert Cecil Martin¹ (widely known as Uncle Bob) introduces several principles in his famous book:

Agile Software Development, Principles, Patterns, and Practices

He described five OOD principles in this book. They are collectively known as the SOLID Principles. These principles have received wide attention in the software industry. The Open-Closed Principle is the second one of the five principles. Otherwise, O of SOLID.

This principle was first coined by Bertrand Meyer² in 1988. He gave a short definition of the principle in his book³:

‘’Modules should be both open and closed.’’

But later, Uncle Bob, in his book, referred to this definition in the following way while describing the Open-Closed Principle:

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

The definition says you should not modify the source code shipped with the package rather it should have a way so that you can add new functionality. This definition tells us two rules to establish onto a class/module/function that we are building as follows:

Open for Extension: A class can be extended according to requirements. You can then add new functionalities. You should design a class in such a way that it has the option to add new functionalities. In other words, you can change what the class does.

Closed for Modification: A class can not be modified. This means you should not modify the source code to add new behaviors. You can not change its already available functionality to add new ones that already work.

First, we would violate the OCP through examples. Then we would see how we can satisfy the OCP. This is actually a better way to understand how a design principle works.

Violating the OCP!

Let us build a simple online store where we would sell books. We need to show information about each book. We may have a Book class as the following with info() method which may display book information.

// TypeScript

class Book {
// Other attributes and methods

// Shows book information
info(): string {
return this._title + ' written by ' + this._author;
}
}
// PHP

class Book
{
// Other attributes and methods

// Shows book information
public function info(): string
{
return $this->title . ' written by ' . $this->author;
}
}

Now imagine, our online store is making good profits. So we decide to add more products to our store. We want to sell movies, songs, and paintings. How would we do that? There are several ways to do that.

First, we can attempt to use the Book class for displaying information about movies. To do so, we need to add getDirector() and setDirector() methods to the Book class. Moreover, we have to modify the info() method of the Book class as the following:

// TypeScript

class Book {
private title: string;

private author: string;

private director: string;

constructor(title: string, author: string, director: string) {
this.title = title;
this.author = author;
this.director = director;
}

// Other accessors and mutators are omitted

public getDirector(): string {
return this.director;
}

public setDirector(director: string): void {
this.director = director;
}

public info(type: string): string {
if (type === 'book') {
return `${this.title} written by ${this.author}`;
} else if (type === 'movie') {
return `${this.title} directed by ${this.director}`;
}

return '';
}
}
// PHP

class Book
{
private string $title;

private string $author;

private string $director;

// Other accessors and mutators are omitted

public function getDirector(): string
{
return $this->director;
}

public function setDirector(string $director): void
{
$this->director = $director;
}

public function info(string $type): string
{
if ('book' === $type) {
return $this->title . ' written by ' . $this->author;
} else if ('movie' === $type) {
return $this->title . ' directed by ' . $this->director;
}
}
}

For the sake of simplicity, we omitted some accessor and mutator methods.

To display information about the movie, we needed to modify the Book class - by adding two more methods and modifying the info() method. This means we went against what the second rule “Closed for Modification” section says. We violated one of the two rules. Thus we can not modify the Book class (the existing code).

Second, we could have extended the Book class to the Movie class to implement the info() method to display movie information. But we can not be able to extend the Book class because a movie is not a book. Even if we do so that will break another principle called LSP.

Third, what if we create a class for each of them? Well, that would be another problem. Each class will then have getTitle() and setTitle() methods several times that make code duplication. At this point, you may guess that the Book class is not open for extension too. It seems we are stuck here.

So there is no proper way to add new products to our shop with our existing code. Thereby the class is not open for extension and is not closed for modification. The design of the Book class violates the OCP therefore.

An abstraction allows for achieving new behaviors of a class without the need to modify its source code. This enables the implementation of additional functionalities without directly altering the original class.

Abstractions

Abstractions are abstract base classes where the unbounded group of possible behaviors is defined for their derivative classes. For example, in an abstract class, you define methods that can be used in the child classes for playing similar roles with different implementations. Basically, an abstract class defines the basic structure of its child classes. So, a class can manipulate an abstraction.

And such a class can be closed for modification because it depends on the abstraction that is fixed. But the behavior of that class or module can be extended by creating new subclasses based on the abstraction.

In our case, we would follow a simple algorithm. We will display the title and the creator of a product. To do so we need an info() method. Let us make an abstraction for displaying product information.

// TypeScript

abstract class AbstractProduct {
private title: string;

constructor(title: string) {
this.title = title;
}

public getTitle(): string {
return this.title;
}

public setTitle(title: string): void {
this.title = title;
}

/**
* Displays information about a product
*/
public abstract info(): string;
}
// PHP

abstract class AbstractProduct
{
private string $title;

public function getTitle(): string
{
return $this->title;
}

public function setTitle(string $value): void
{
$this->title = $value;
}

/**
* Displays information about a product
*/
abstract public function info(): string;
}

Notice we abstract away the info() method to an abstract class called AbstractProduct. This method plays similar roles with different implementations through derived classes. Therefore, it is not tied to a specific implementation. That is why it has been made abstract.

Languages like Java, C#, PHP, TypeScript, etc. have abstract keyword to create abstract classes and methods.

AbstractProduct is now such a class that is now conforming to OCP. How? Try to remember what the abstraction and the OCP describe above. This class is now open for extension and closed for modification. Because the abstraction is fixed and if we need to add more behaviors it will allow us to do by creating subclasses. How? Check out the following two code examples.

Now let us create our Book class depending on the abstraction:

// TypeScript

class Book extends AbstractProduct {
private author: string;

constructor(title: string, author: string) {
super(title);
this.author = author;
}

public getAuthor(): string {
return this.author;
}

public setAuthor(author: string): void {
this.author = author;
}

info(): string {
return this.getTitle() + ' written by ' + this.author;
}
}
class Book extends AbstractProduct 
{
private string $author;

public function getAuthor(): string
{
return $this->author;
}

public function setAuthor(string $value)
{
$this->author = $value;
}

public function info(): string
{
return $this->getTitle() . ' written by ' . $this->author;
}
}

A book is a product. A Book can extend AbstractProduct. We add the accessor and the mutator methods for author attribute to display title and author of a book.

Now if we want to display information about a movie, that would be a new product. To do so we do not have to modify the info() method of the Book class anymore. We can depend on the abstraction. We can add that product to our shop by creating a new subclass. Let us create a new product type called movie:

// TypeScript

class Movie extends AbstractProduct {
private director: string;

constructor(title: string, director: string) {
super(title);
this.director = director;
}

public getDirector(): string {
return this.director;
}

public setDirector(director: string): void {
this.director = director;
}

info(): string {
return this.getTitle() + ' directed by ' + this.director;
}
}
// PHP

class Movie extends AbstractProduct
{
private string $director;

public function getDirector(): string
{
return $this->director;
}

public function setDirector(string $value): void
{
$this->director = $value;
}

public function info(): string
{
return $this->getTitle() . ' directed by ' . $this->director;
}
}

A movie is also a product. A Movie can also extend AbstractProduct. Like Book, we add accessor and mutator methods for director attribute to display movie name and director.

We are now able to add new behaviors depending on the abstraction. Using this approach we can now use the info() method for different products or implementations. Therefore, we have been now able to close the base abstract class for modification. Thus we satisfy the OCP.

The method, we followed to conform to the OCP, is called Template Method Pattern⁴. That means we implemented this design pattern here to satisfy OCP. GoF has given a definition of this pattern in the book as follows:

‘’Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure.’’

In the example above, we made the skeleton in our AbstractProduct class. You may think of the info() method as the skeleton of an algorithm (displaying product information). Later, we redefined this method in different classes to display product-specific information.

Interestingly, implementing Template Method Pattern, we implemented two more OOP principles: Polymorphism⁵ and Program to an Interface, not an Implementation. There is yet another way to satisfy OCP called Strategy Pattern⁶. GoF has given a definition of this pattern in the book as follows:

‘’Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.’’

We can satisfy the OCP with this design pattern too. But I am leaving Strategy Pattern implementation to satisfy OCP to you. I want you to dive into this pattern and satisfy the OCP using it.

Applying OCP

Note that we kept our code as simple as possible to explain the OCP. We could have added or removed more attributes and methods to those classes above if we had built a real application. Now we would see how the OCP may be useful to write reusable, scalable, and maintainable code. See the following Product class.

// TypeScript

class Product {
private product: AbstractProduct;

constructor(product: AbstractProduct) {
this.product = product;
}

public info(): string {
return this.product.info();
}
}
class Product 
{
private AbstractProduct $product;

public function __construct(AbstractProduct $product)
{
$this->product = $product;
}

public function info(): string
{
return $this->product->info();
}
}

The Product class is requiring the AbstractProduct class as a dependency through its constructor method. And it is delegating the task of displaying product information via its info() method to the info() method of an object that will be passed as a constructor’s argument, such as a Book object.

This product class may be used throughout your whole application to display product information. Now if you needed to add new products you would not require to modify your client code meaning each place where the Product class has been used. All you need to tell your service provider, say, the dependency injection manager, to send an object to the instance of the Product class. On the other hand, if you wanted to change a product object that would not make any changes to the client code. So this is a great way to write scalable and reusable code.

A simple client code example:

// TypeScript

// Creates book object
const book = new Book(
'Patterns of Enterprise Application Architecture',
'Martin Fowler'
);

// Creates movie object
const movie = new Movie('Forrest Gump', 'Robert Zemeckis');


// Create more product objects and register with service provider


const movieObject = serviceProvider.get('movie');
const product = new Product(movieObject);

// Shows info about book
console.log(product.info());
// PHP

// Creates book object
$book = new Book();
$book->setTitle('PHP Objects, Patterns, and Practice');
$book->setAuthor('Matt Zandstra');

// Creates movie object
$movie = new Movie();
$movie->setTitle('Forrest Gump');
$movie->setDirector('Robert Zemeckis');


// Create more product objects and register with service provider


$movie = $serviceProvider->get('movie');
$product = new Product($movie);

// Shows info about book
echo $product->info() . PHP_EOL;

See full code examples here: TypeScript and PHP.

References:

  1. Robert Cecil Martin
  2. Bertrand Meyer
  3. Object-Oriented Software Construction
  4. Template Method Pattern
  5. Polymorphism
  6. Strategy Pattern

--

--

Abu Jobaer

Advocate Clean Code, SOLID Principles, Software Design Patterns & Principles, and TDD.