What Is The Open / Closed Principle?


4 min read

This week at work, the Open / Closed Principle came up in conversation. I was first introduced to it during my apprenticeship at 8th Light. During that moment, I couldn't remember what the principle was, or how to apply it. In this blog post, I'll share what I've learnt this week about the Open / Closed principle.

The Open / Closed principle is one of the SOLID principles in object oriented programming, and is defined as:

“Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.” — Bertrand Meyer

You may be wondering how an entity can be open and closed at the same time. The key words here are extension and modification.


Open For Extension & Closed For Modification

Open for extension relates to being able to add to an entity in the form of an add on. As a result, its behaviour is extended, without being modified directly. Closed for modification refers to changes which are not applied to the entity directly. Let's look at a real world example, using a wall plug socket:

The wall plug socket is currently equipped to provide power through one plug. We currently have one appliance, a toaster, and plugging it into the wall socket provides it with power. Pretty straightforward.

We have obtained another electrical appliance, a coffee machine, which also needs power. The issue is we only have one wall socket, and two appliances which require power at the same time.

One alternative is changing the single wall socket to a double socket. This modification will provide power for two appliances (let's ignore the wiring aspect).

What will need to happen if we obtain another 4 electrical appliances, which all need power at the same time? It will require changing the wall socket to accommodate 4 separate plugs, and so on as we obtain more appliances. This is a modification and not an extension. Its behaviour has changed, as it now provides power via numerous wall sockets, rather than one.

Another alternative is keeping the single wall socket, and plugging in an extension cord with numerous plug sockets on it. As a result, we haven't modified the wall socket.

We have extended the behaviour of it to provide power to numerous appliances, instead of one. The behaviour of the wall socket has not changed, it still provides power via the single wall socket. We can also plug in an extension cord with even more plug sockets, and the behaviour of the single wall socket will not change.

A significant benefit of implementing the principle is bugs are less likely to be introduced. This is due to the entity not being modified, and consequently can be said to be reliable, robust and reusable due to its behaviour remaining consistent.


Violation of the principle

Let's look at an example of Kotlin code, which is open for modification, and violates the principle. Below we have a class named Zoo, which is initialised with an array of strings, containing types of animals. Within the class, there is a method feedAnimals(), which feeds all of the animals within our array.

1class Zoo constructor(private val animals: Array<String>) {
2
3 fun feedAnimals(): Unit {
4 for (animal in animals) {
5 when (animal) {
6 "giraffe" -> println("Feeding the giraffe acacia leaves.")
7 "penguin" -> println("Feeding the penguin krill.")
8 else -> println("Feeding the $animal nothing.")
9 }
10 }
11 }
12}
1fun main(args: Array<String>) {
2 val myAnimals = arrayOf("giraffe", "penguin", "lion")
3 val myZoo = Zoo(myAnimals)
4
5 myZoo.feedAnimals()
6}
1-- output from running the main function --
2
3Feeding the giraffe acacia leaves.
4Feeding the penguin krill.
5Feeding the lion nothing.

We have three animals, and we have a method which feeds the giraffe and penguin. Other animals which are not specified in feedAnimals() are not fed.

We decide the lion needs to eat. In order to do this, we need to amend feedAnimals() accommodate the lion.

1class Zoo constructor(private val animals: Array<String>) {
2
3 fun feedAnimals(): Unit {
4 for (animal in animals) {
5 when (animal) {
6 "giraffe" -> println("Feeding the giraffe acacia leaves.")
7 "penguin" -> println("Feeding the penguin krill.")
8 "lion" -> println("Feeding the lion raw meat.")
9 else -> println("Feeding the $animal nothing.")
10 }
11 }
12 }
13 }
1-- output from running the main function --
2
3Feeding the giraffe acacia leaves.
4Feeding the penguin krill.
5Feeding the lion raw meat.

The change we have implemented, means the lion is fed. That's great, but what would we need to do if we added 100 animals to our zoo, and wanted to feed them all? The feedAnimals() method would need to be changed to accommodate them all. Similarly, if we wanted to change what the giraffe and penguin are fed, or stop feeding them, the method would need to be changed.

This example is a violation of the Open / Closed principle, because feedAnimals() needs to be modified when the requirements of our zoo changes.


Adhering To The Principle

Let's change our example to adhere to the principle. In the following example, we will use an interface, and create classes for the animals in our zoo.

1interface Animal {
2 fun feed()
3}
4
5class Giraffe: Animal {
6 override fun feed() {
7 println("Feeding the giraffe acacia leaves.")
8 }
9}
10
11
12class Penguin: Animal {
13 override fun feed() {
14 println("Feeding the penguin krill.")
15 }
16}

Note: Read more about interfaces here

The Giraffe and Penguin classes are types of animals. They implement the Animal interface, and the feed() method.

Let's change our main() method to initialise instances of our animal classes, and our Zoo class to accommodate them.

1fun main(args: Array<String>) {
2 val gina = Giraffe()
3 val paul = Penguin()
4
5 val myAnimals = arrayOf(gina, paul)
6 val myZoo = ZooTwo(myAnimals)
7
8 myZoo.feedAnimals()
9}
1class ZooTwo constructor(private val animals: Array<Animal>) {
2
3 fun feedAnimals(): Unit {
4 for (animal in animals) animal.feed()
5 }
6}

Changes to the Zoo class:

  • Is initialised with an array of Animals, instead of an array of strings
  • feedAnimals() still loops through the array, but now calls the animal's feed() method.

When we run main() the output is:

1-- output from running the main function --
2
3Feeding the giraffe acacia leaves.
4Feeding the penguin krill.

The above changes have allowed our Zoo class to be closed for modification, and open for extension. But how? Let's see what happens when we add another animal to the zoo.

To add another animal (a lion in this case):

  • Create a Lion class (which implements the Animal interface)
  • Add a new instance of Lion to our array of animals
1class Lion: Animal {
2 override fun feed() {
3 println("Feeding the lion raw meat.")
4 }
5}
1fun main(args: Array<String>) {
2 val gina = Giraffe()
3 val paul = Penguin()
4 val lauren = Lion()
5
6 val myAnimals = arrayOf(gina, paul, lauren)
7 val myZoo = ZooTwo(myAnimals)
8
9 myZoo.feedAnimals()
10}
1-- output from running the main function --
2
3Feeding the giraffe acacia leaves.
4Feeding the penguin krill.
5Feeding the lion raw meat.

We've added a lion, which can be fed. The difference between this example, and the one in the previous section is we didn't need to modify feedAnimals() to accommodate our new animal

As a result, the method is closed for modification, and open for extension, adhering to the Open / Closed Principle.


View the code on github: https://github.com/ellehallal/kotlin-open-closed-principle-example

Previous post:
Git - Delete All Branches, Except Master
Next post:
Python - Pyenv Commands

Discussion