Open/Closed Principle
Introduction
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.