Este post también está disponible en español
This principle states that an entity (class, module, function, etc) must be open for extension but closed for modification.
We must be able to extend the behavior of our entities, without the need to modify its code.
When we say that an entity must be open for extension, we mean that it must be able to adapt to the changes and needs of an application. If we talk about the other part, closed for modification, we mean that the adaptability of such entity should not be the result of the modification of its core.
Applying this concept correctly provides us with software that is easier to maintain and extend functionalities by minimizing changes in the code base and without modifying basic parts of the application. It also avoids generating new bugs in functionalities that were already developed, tested and working correctly.
How can we detect that we are violating this principle?
If every time there is a new requirement or a modification of the existing ones, the same entities are affected, we can begin to identify a violation of this principle.
Example
For our example we are going to use a Vehicle class, which must have the ability to draw the type of vehicle that is passed by parameter.
For this, we have the following structures:
class Vehicle(val type: VehicleType)
enum class VehicleType {
CAR, MOTORBIKE
}
Let’s see the method in charge of performing the aforementioned functionality:
fun draw(vehicle: Vehicle) {
when(vehicle.type) {
VehicleType.CAR -> drawCar(vehicle)
VehicleType.MOTORBIKE -> drawMotorbike(vehicle)
}
}
With the proposed structure, every time we need to add a new vehicle it would imply creating a new enum, a new case for that enum, and a new method to implement the drawing of the new case. This way we would be violating the Open-Closed principle, since in each new case we would need to modify the code base of the Vehicle class.
Let’s see how we solve this problem by means of polymorphism, making each enum a concrete class and knowing how to draw itself.
For this, we convert the Vehicle class into an interface with the draw method and each enum into a concrete class, which implements our interface:
interface Vehicle {
fun draw()
}
class Car : Vehicle {
override fun draw() {
// Draw the car
}
}
class Motorbike : Vehicle {
override fun draw() {
// Draw the motorbike
}
}
Thus, our previous method reduces to:
fun draw(vehicle: Vehicle) {
vehicle.draw()
}
If we go back to the previous case, in which we had the need to add new requirements, now we can do it without touching the Vehicle code base. Adding new vehicles is as simple as creating the corresponding class and having it implement our interface:
class Truck: Vehicle {
override fun draw() {
// Draw the truck
}
}
Conclusion
The Open/Closed principle is an indispensable tool to protect us against changes in modules or parts of code where such modifications are frequent.
Having code closed to modification and open to extension gives us maximum flexibility with minimum impact.