Welcome back! In today’s article, we will talk about the Open Close Principle. If you’ve missed the previous article on the Single Responsibility Principle, be a good platypus and make sure to check it out before you continue!
Let us begin with the oh-so-dry definition of the Open Close Principle:
“A class should be open to extension, but closed for modification.”
In simple platypus language:
“Every time something new is introduced, you should be writing new code, not re-writing old code!“
This time, Perry (yes, the platypus is called Perry) wants to organise a road-trip into the Himalayas. He gathers all his other friends and wants to ensure that everyone’s car can make the 3,141km journey across the mountains.
Being a lazy platypus, he’s going to use a MileageCalculator
to help him!
class Car {
// The fuel consumption of the vehicle in km/litres
public double fuelConsumption;
// The capacity of the fuel tank in litres
public double fuelTankCapacity;
}
public class MileageCalculator {
public boolean allCarsHaveEnoughMileage(Car[] cars) {
boolean allCanMakeIt = true;
for (Car c : cars) {
boolean currentCarCanMakeIt =
c.fuelConsumption * c.fuelTankCapacity >= 3141;
allCanMakeIt = allCanMakeIt && currentCarCanMakeIt;
}
return allCanMakeIt;
}
}
Beautiful! Now Perry will be able to make sure everyone can make it for the arduous journey!
But wait…
His friend Bucky has just bought a new Tesla! Teslas don’t have fuel tanks, right? Oh gosh…
Flustered, Perry tries to fix the MileageCalculator
to make sure Bucky’s new Tesla isn’t left out.
interface Car {
}
class FuelCar implements Car {
// The fuel consumption of the vehicle in km/litres
public double fuelConsumption;
// The capacity of the fuel tank in litres
public double fuelTankCapacity;
}
class ElectricCar implements Car {
// The distance the car can go in one full charge, in km
public double rangePerCharge;
}
public class MileageCalculator {
public boolean allCarsHaveEnoughMileage(Car[] cars) {
boolean allCanMakeIt = true;
for (Car c : cars) {
if (c instanceof FuelCar) {
FuelCar fc = (FuelCar)c;
boolean canMakeIt =
fc.fuelConsumption * fc.fuelTankCapacity >= 3141;
allCanMakeIt = allCanMakeIt && canMakeIt;
} else if (c instanceof ElectricCar) {
ElectricCar ec = (ElectricCar)c;
boolean canMakeIt = ec.rangePerCharge >= 3141;
allCanMakeIt = allCanMakeIt && canMakeIt;
}
}
return allCanMakeIt;
}
}
Nasty! A terrible attempt at trying to re-write old code! Imagine if Perry was maintaining a 10,000-line long codebase. Can you even imagine how many lines he has to rewrite?
I guess this is the perfect time to teach him how to use the Open Close Principle!
“Hey Perry, since you want to get the mileage of each Car, you should make all Car objects have a getMileage
method. When checking through the list of all cars, you just have to use getMileage
and compare it with 3,141km, regardless whether the car is a fuel car or an electric car.”
“That’s so smart!”
interface Car {
public double getMileage();
}
class FuelCar implements Car {
// The fuel consumption of the vehicle in km/litres
public double fuelConsumption;
// The capacity of the fuel tank in litres
public double fuelTankCapacity;
public double getMileage() {
return fuelConsumption * fuelTankCapacity;
}
}
class ElectricCar implements Car {
// The distance the car can go in one full charge, in km
public double rangePerCharge;
public double getMileage() {
return rangePerCharge;
}
}
public class MileageCalculator {
public boolean allCarsHaveEnoughMileage(Car[] cars) {
boolean allCanMakeIt = true;
for (Car c : cars) {
boolean canMakeIt = c.getMileage() >= 3141;
allCanMakeIt = allCanMakeIt && canMakeIt;
}
return allCanMakeIt;
}
}
Again, for those who like to look at diagrams, here you go:
Now, even if someone were to invent a nuclear-powered car with a totally different way of calculating its mileage, we still wouldn’t have to touch our MileageCalculator
at all!
Open For Extension
What does it mean for a code to be “open for extension”? To put it simply, we can use it as-is in our bigger picture without having to change it, even when requirements change. Say for instance you want a brighter desk lamp. Should you change the bulb to have a higher wattage? Or would it be smarter to buy one of those dimmable light bulbs from IKEA that can allow you to change its brightness at your command?
In the simple example above, we can extend the bulb’s functionality if we had designed it to be dimmable so that whenever we want the room to be brighter, we won’t have to change the bulb to a brighter one.
Close For Modification
Using the same example above, we see that we shouldn’t need to meddle with the electrical wirings within the bulb just to make it brighter. Just design it within such that we can control its behaviour from the outside.
How to follow the Open Close Principle
General steps for any platypus/human to follow:
- Design your code with a hierarchy in mind. Start from the most general class and slowly add more and more specific code as we extend downwards. In the example above, we see that we could have classified both
FuelCar
andElectricCar
asCar
, and soCar
will be the most general class. In this way, any extension to the functionality of the code will be an extension onCar
, rather than a modification onCar
. This helps us to be open to extensions. - Do not be too specific. Being specific means that if the requirements change, then there is a high chance we need to modify our code, which we don’t want. We want to add new requirements as a form of extensions, which means that we are writing new code instead of re-writing old code! This is closed for modification at its finest.
Happy Coding!