Skip to main content

Open/Closed Principle

Introduction

Traffic Barrier Icon

The Open/Closed Principle is the second principle in the SOLID acronym. It was introduced by Bertrand Meyer in 1988. The principle states:

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

What this means is that we should write code so that it it does not need to be changed whenever a new requirement is added. The main goal of this principle is to prevent the existing code from breaking when new features are added.

A class is:

  • Open if it can be extended by creating a new subclass where you can create new methods or override the existing behavior.
  • Closed if it is already in use by other classes, it's interface is clearly defined and won't be changed in the future.

Your classes should be both Open to extension, and Closed to modification.

Advantages

This principle is all about minimizing risk. If some class is already implemented, tested, reviewed, and in use in the application, then making changes to it could be very risky.

If you can implement a feature by adding a subclass and overriding the parts of the original code that you need to change, or extending the functionality and add your own methods instead of modifying the existing code, then you can achieve your goals without risking breaking the existing logic.

Note that this principle does not apply all changes. Bug fixes should always be addressed directly.

Example

Let's say we have a simple Rectangle class that has a width and height property. We also have an AreaCalculator class that calculates the total area for a list of rectangles. We can calculate the area of each rectangle by multiplying the width and height and sum the results.

class Rectangle(val width: Double, val height: Double)

class AreaCalculator {
fun calculateArea(rectangles: List<Rectangle>): Double {
var totalArea = 0.0
for (rectangle in rectangles) {
totalArea += rectangle.width * rectangle.height
}
return totalArea
}
}

This implementation works just fine, but what if we want to add a new shape, like a Circle? If we want the total area of a list that contains both circles and rectangles, we would need to add a Shape interface and modify the AreaCalculator class to handle the new shape.

interface Shape 

class Rectangle(val width: Double, val height: Double) : Shape

class Circle(val radius: Double) : Shape

class AreaCalculator {
fun calculateArea(shapes: List<Shape>): Double {
var totalArea = 0.0
shapes.forEach { shape ->
when(shape) {
is Rectangle -> totalArea += shape.width * shape.height
is Circle -> totalArea += shape.radius * shape.radius * Math.PI
}
}
return totalArea
}
}

While this works, the approach of checking the type of the shape and calculating the area based on the type is not ideal. Instead, we can use the Open/Closed Principle to make the AreaCalculator class open for extension and closed for modification.

interface Shape {
fun calculateArea(): Double
}

class Rectangle(val width: Double, val height: Double) : Shape {
override fun calculateArea(): Double {
return width * height
}
}

class Circle(val radius: Double) : Shape {
override fun calculateArea(): Double {
return radius * radius * Math.PI
}
}

class AreaCalculator {
fun calculateArea(shapes: List<Shape>): Double {
return shapes.sumOf { it.calculateArea() }
}
}

With these changes adding new shapes no longer requires modifying the current implementation. Which means that this code does not need to be completely retested every time a new shape type is added.

Where did we go wrong?

While the initial implementation was not open for extension, it wasn't really a problem. Since there was only a single shape, we didn't need anything more complex. The issue really came up when we needed to add a new shape. Being asked to add a new shape implies that more shapes could be added in the future. This is where the Open/Closed Principle comes into play.

Conclusion

The Open/Closed Principle is all about minimizing risk. By making your classes open for extension and closed for modification, you can add new features without risking breaking the existing code. By embracing this principle, you will limit the number of tests you need to run and the amount of code you need to review, which will save you time and reduce the risk of introducing bugs.

Image Credits

Road-block icons created by Witdhawaty - Flaticon