What is the Open Closed Principle?

SOLID

This is a continuation of posts on the SOLID principles. Find the rest here.

Open Closed Principle

The Open-closed Principle (OCP) is, in a similar fashion to SRP, about minimising the damage that making code changes does to a project. A nasty smell that would indicate that some software is in need of OCP is cascading changes - a simple change to the source code results in a cascade of changes to other parts of the code and their dependencies. When the principle is applied, a required change to software would result in new code rather than modification to old code.

Definition

So this takes us to the definition of the principle itself. It says that software should be open for extension, but closed for modification. Initially it can seem like these two rules counteract each other - how do you extend software without modifying it? The answer to this is to create extensible classes that allow for any new kind of behaviours by creating implementations of that class - in OOP terms, abstraction.

There are many ways to do this, and some really nice patterns exist that are made with this exact idea in mind. Two I have used recently to implement the ideas of OCP are strategy and observer patterns.

The strategy pattern gives a nice way of extracting behaviours into multiple child classes, whilst defining a common strategy interface that the “client” of the strategy can use. The observer pattern is more of an event-driven pattern that lets you notify different "subscribers" of state change and the subscribers that need to react to that state change can do so.

Example

In a project I’ve been working on recently, I used some kind of amalgamation of these two patterns in a client-server architecture to allow for extensible responding to packets in the server from the client.

The project uses socket.io for its socket handling, which uses an .on function for responding to events it receives on an active connection. It looks like this:

1
2
3
socket.on("details", (...args) => {
// a function to run when the event is received
});

In this example, the socket is an active connection from a client, and we’re reacting to a "details" event.

From the documentation, it would seem the standard expectation would be to write multiple .on functions to react to individual events. This means we could quite easily, and quickly, write a fully-functional server that responds to the clients events without much fuss.

If we wrote this code, we’d probably end up with something that looked like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function handleDetails(...args) {
// Handle details
}

function handleEvent(...args) {
// Handle event
}

function handleDisconnect(...args) {
// Handle disconnect
}

io.on("connection", (socket) => {
socket.on("details", handleDetails);

socket.on("event", handleEvent);

socket.on("disconnect", handleDisconnect);

// ... and so on
});

However, if a new requirement was made for some extra features in the client and the client would be sending new events to the server, this change would require going into the server and modifying the code. But this breaks OCP, the more we go into this code and change or add features, the more bloated it is going to get and the more dreaded cascading changes we might see.

My approach to apply OCP for this was to create an EventHandler class that acts as the Publisher in the Observer pattern:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export class EventHandler {
subscribers: GameEventListener[] = [];

subscribe(subscriber: GameEventListener): void {
// add to subscribers
}

unsubscribe(subscriber: GameEventListener): void {
// remove from subscribers
}

notify(...args) {
this.subscribers.forEach(subscriber => {
subscriber.execute(...args);
});
}
}

Observers to this publisher are of type GameEventListener - this is the abstract class that allows for endless extensibility in event listeners. Every listener that wants to implement some behaviour that reacts to an event must extend GameEventListener:

1
2
3
4
5
6
7
8
9
10
11
export abstract class GameEventListener {
eventHandlers: Handler[];

execute(...args): void {
this.eventHandlers.forEach((eventHandler) => {
if (event === eventHandler.event) {
eventHandler.handler(...args);
}
});
}
}

The base GameEventListener implements an execute function that loops through the event listener’s handlers, Handler is a class with two properties:

1
2
3
4
class Handler {
event: string;
handler: Function;
}

When any event is received by socket.io, it is sent to the event handler’s execute function:

1
2
3
4
5
6
7
8
io.on("connection", (socket: Socket) => {
socket.emit('matchmaking');

/* Send events to an event hanlder */
socket.onAny((eventType, args) => {
this.eventHandler.notify(user, gameRoom, eventType, args);
});
});

The .onAny function allows me to send all events through to the event handler, and eventType is the string for that event - so will be able to match on a Handler.event string!

Implementations of the GameEventListener abstract class are where things start to take a turn into the Strategy pattern - each child event listener defines its own “strategy” of how to deal with an event, depending on what the event is. Taking the event listeners from the first example, this means you could have a DetailsEventListener class, a EventEventListener and a DisconnectEventListener - these would all define their own behaviour for each event - AKA their strategy. So I end up with a nice mix of patterns that adheres to OCP and helps my use case.

Back to the GameEventListener.execute() function - it loops through every implementation’s event handlers and tries to find a matching event. If it finds one, the function for that handler is executed. This means that if I ever want to add new functionality to my event listeners all I need to do is write a new class. It will implement GameEventListener, write a handler function for an event, and then subscribe that handler to the EventHandler class. There is no need to modify any existing code for new functionality, just create new code - OCP!

A simple example of an implementation of a GameEventListener, AKA a strategy:

1
2
3
4
5
6
7
8
9
10
11
12
export class DisconnectListener extends GameEventListener {
eventHandlers: Handler[] = [
{
event: "disconnect",
handler: this.disconnectHandler
}
];

disconnectHandler(...args) {
console.log(`${user.name} has disconnected`);
}
}

This is a strategy I have in my server, with some details abstracted. The DisconnectListener extends GameEventListener meaning it keeps that execute function - it then registers the event it listens to by adding a Handler to its eventHandlers array. It then subscribes to the EventHandler (my publisher) back in the main file:

1
this.eventHandler.subscribe(new DisconnectListener());

Now, whenever the server receives a disconnect event, the event handler pushes that event to its subscribers, who decide if they want to handle it or not. The DisconnectListener strategy has some defined behaviour for that event, so its function is executed and it handles the event.

This means that any new functionality can be added simply by creating new classes, without worrying about modifying existing code.