The Open Closed Principle Revisited

2019-03-12

An interesting discussion came up in a recent code review. Looking over the OOP implementation of a menu class, a colleague remarked that classes should always be designed open for extension and closed for modification. Sounds right, as it reflects generally accepted OOP practices. The open-closed principle, the “O” in SOLID, is an essential principle of sound object-oriented design. You probably already knew that. Here is the initial design of that class.

1
2
3
4
5
6
7
8
class Menu {
private constructor()
static createForUser(user: User): Menu
static createForLegacyUser(user: LegacyUser): Menu
public getThisMenu(): MenuData
public getThatMenu(): MenuData
public getAnotherMenu(): MenuData
}

The menu class can be instantiated in two different ways, either with a regular user or with a legacy user. Both user objects provide authorization via ACL. The menu class has several getters that return different menus. The dynamically created MenuData contains only the menu options the user has access to. The private implementation methods are omitted. These perform authorization using the user objects and also translate the menu items into the user’s UI language. The menus themselves are configured in external YAML files and their structure can be changed as long the expected YAML format is maintained.

As you can gather from the design, this class is not designed with extension in mind. It is possible to add other menus, but only by extending it and adding a getYetAnotherMenu() method. Unfortunately, inheritance is not very practicable here. Extending for other types of users isn’t possible at all, because the respective authorisation code would have to be added to the implementation. Let’s summarise:

  1. Very easy: adding or removing menu options and sub menus to the existing menus
  2. Possible, but not that easy: adding or removing menus
  3. Not possible: adding other types of users

open doors

An obvious weakness of the above design is the redundancy in the getters and the fact that the menus themselves are fixed. This can be improved quite easily:

1
2
3
4
5
6
class Menu {
private constructor()
static createForUser(user: User) : Menu
static createForLegacyUser(user: LegacyUser): Menu
public getMenu(menuIdentifier: string): MenuData
}

Instead of invoking it with menu = Menu.createForUser(user).getThisMenu() to obtain menu data, this class is now invoked with menu = Menu.createForUser(user).getMenu(‘thisMenu’). The identifier is associated with the respective YAML file from which the menu data structure is created. Let’s summarise again:

  1. Very easy: adding or removing menu options and sub menus to the existing menus
  2. Very easy: adding or removing menus
  3. Not possible: adding other types of users

At this point, I was quite happy with the simplicity and flexibility of the design. We’ve made it both easy to extend menu options as well as menus themselves. My colleague, however, wasn’t happy. He insisted that the open-closed principle must also be applied to user types. And this is where it gets philosophical. Of course, it is possible to do that. But it requires significant changes and a further abstraction. We need to remove the authorisation implementation from the menu class, create an Authoriser interface and provide Authoriser classes for regular users and legacy users that implement Authoriser.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Menu {
static create(authoriser: Authoriser) : Menu
public getMenu(menuIdentifier: string): MenuData
}

interface Authoriser {
boolean authorise(role: AclRole)
}

class RegularUserAuthoriser implements Authoriser {
constructor(user: User)
authorise(role: AclRole)
}

class LegacyUserAuthoriser implements Authoriser {
constructor(user: LegacyUser)
authorise(role: AclRole)
}

Why philosophical? Because, while this is a textbook implementation of the open-closed principle, we have never added any other user types in years. The system has only two types of users: regular users and legacy users. The likelihood of having other types of users in future is almost nil. This raises two questions: First, is the cost of extensibility and extra abstraction justified? This change obviously makes the code more complex. Second, how do you know what kind of extensibility will be required in future?

I would answer the first question with “no”. It is not worthwhile complicating the design to achieve extensibility if this extensibility is not needed. Doing so would constitute a YAGNI antipattern. YAGNI stands for “you aren’t gonna need it” and originates from the core principles of extreme programming (XP). According to XP, features should be implemented as simple as possible and without making unwarranted assumptions about future use cases. This is very much related to the KISS principle, which in turn has its foundation in philosophy: Occam’s razor and the law of parsimony. In practical terms: the simplest implementation for a given requirement is the best implementation.

If you paid attention, you might already see the dilemma. There seems to be a contradiction between YAGNI and the open-closed principle. If we design a class that is open for extension and closed for modification, this usually means we have to define additional interfaces that formalise the extensible behaviour. This is to say, the open-closed principle results in increased complexity, contradicting KISS, YAGNI and Occam’s razor. So, what should we do?

Consider the second question we have asked before: how do you know what kind of extensibility will be required in future? Simply speaking, if you don’t know, then don’t build it. Classes usually contain more than a sinlgle dependency and a single behaviour. Designing extensibility into each dependency and behaviour is a very bad idea, because it would make the design extremely complex. The open-closed principle should be applied in situations where the need for extensibility is either established or very likely. The “O” in SOLID must not be applied indiscriminately..

Contemplate the following quote of former US secretary of defense, Donald Rumsfeld:

There are known knowns. There are things we know that we know. There are known unknowns. That is to say, there are things that we now know we don’t know.. But there are also unknown unknowns. There are things we do not know we don’t know.

At first, this statement sounds a bit perplexing, but it makes perfect sense. It can be applied to our OOP problem as follows:

The known knowns are the features described in the functional and technical specifications. They are spelled out unambiguously and can therefore be translated directly into concrete implementations. The known unknowns are the features that we can predict, but their concrete implementation is unknown. This is where the open-closed principle applies. For example, if we write a printer spooler, we know that the spooler should work with all sorts of printers, although we don’t know the concrete printer models. Therefore, we provide an interface that can be implemented for any concrete printer model which he spooler then uses to perform its function. Finally, the unknown unknowns are the sort of features that we cannot predict at all.

As mentioned, designing extensibility for unknown unknowns results in unneeded complexity. The same is true for the known knowns since they are fixed. There is no need to design an interface for a class if we already know that there will be only one concrete implementation. Hence, the open-closed principle makes sense only for the known unknowns, namely the situations where feature extensions can be predicted with some degree of certainty.