SOLID: Open Closed Principle
Victor Manuel Pinzon
Posted on April 8, 2021
This is a continuation of the SOLID principles series.
The Open/Closed Principle (OCP) is one of the most important SOLID principles but the least used. One of the reasons is its ambiguous and confusing definition. The OCP was first defined by Bertrand Meyer in his “Object-Oriented Software Construction” book. The OCP, according to Meyer, states the following:
Software entities should be open for extension but closed for modification.
The original definition given by Meyer led to some confusion within the developer community because it was poorly phrased. Years later, Bob Martin expanded the definition as follows:
You should be able to extend the behavior of a system without having to modify that system.
This definition was in tune with the plug-in architecture, which was at its peak at that time. The plug-in architecture states that you should build software that is able to extend its core functionality by providing plugins, which you could add, remove or change with little or no effect on the rest of the core system.
For instance, an integrated development environment (IDE) allows you to extend its core functionality by using plugins, such as Lite Server, Remote SSH, etc... These plugins do not affect the IDE’s core system. This means that an IDE is in tune with the plug-in architecture and the Open/Closed Principle.
Another example of OCP can be found in software libraries. These are usually developed to be used in different contexts and projects. Some standard libraries even allow you to extend their core functionality by using inheritance and polymorphism. These libraries are also in tune with the Open/Closed Principle because you could extend the functionality of these libraries without affecting their core functionality.
Now let's see a code example. Suppose you work for a gaming development company. Your boss, as part of a project, asks you to implement a library for calculating any rectangle´s area. This will be used for drawing the game’s characters.
You develop the following classes:
public class Rectangle{
private final double width;
private final double height;
public Rectangle(double width, double height){
this.width = width;
this.height = height;
}
/*Getters / Setters*/
}
The Rectangle class stores the width and height of any rectangle.
public class AreaCalculator{
public double calculateArea(Rectangle rectangle){
return rectangle.getWidth * rectangle.getHeight;
}
}
The AreaCalculator class defines just one method in charge of the area calculation, which is done by multiplying the rectangle’s width by the rectangle’s height.
The library is used as follows:
public class App{
public static void main(String[] args){
AreaCalculator areaCalc = new AreaCalculator();
Rectangle rec1 = new Rectangle(13.5, 14);
Rectangle rec2 = new Rectangle(7.89, 9.85);
System.out.println("Rectangle's area #1: " + areaCalc.calculateArea(rec1));
System.out.println("Rectangle's area #2: " + areaCalc.calculateArea(rec2));
}
}
Your library is a success and is used in multiple projects. Months later, your boss asks you to implement the area calculation for squares.
When you start the analysis of the new requirement you realize the following:
- A square is a special case of the rectangle. The difference is that the sides of the square are the same length.
- The area of a square is calculated by multiplying the length of the sides by itself.
You immediately realize that your library does not comply with Open/Closed Principle, because you cannot extend the library functionality by adding new geometric figures without modifying the existing code. Of course, we could discuss that in order to add the square functionality you could set the same length for all the sides of the rectangle, but what if you want to add the circle functionality?
The answer is the Open/Closed Principle.
Applying the Open/Closed Principle
The Area Calculator library shows us the problems that can arise when we do not take into account the future changes that a specific module can have. The main objective of the OCP is to develop software solutions where you can easily extend functionalities without affecting the core system. To achieve this, you must identify the modules in your code that are most likely to change in the future and decouple them, so you can easily extend their behavior.
Decoupling means taking the key parts of your classes’ functionality and replace them with interfaces. This allows you to replace these key parts with any class that extends the interface.
To apply OCP in the Area Calculator library, we’ll define a geometric figure interface that will have just one method for area calculation.
public interface GeometricFigure{
public double calculateArea();
}
Then we’ll define the specific geometric figure classes. Each figure will define its own implementation of the area calculation.
public class Rectangle implements GeometricFigure{
private final double width;
private final double height;
public Rectangle(double width, double height){
this.width = width;
this.height = height;
}
@Override
public double calculateArea(){
return this.width * this.height;
}
}
public class Square implements GeometricFigure{
private final double side;
public Square(double side){
this.side = side;
}
@Override
public double calculateArea(){
return this.side * this.side;
}
}
public class Circle implements GeometricFigure{
public static final double PI = 3.1416;
private final double radius;
public Circle(double radius){
this.radius = radius;
}
@Override
public double calculateArea(){
return (Circle.PI * (this.radius * this.radius));
}
}
We will modify the AreaCalculator class so that the calculateArea method receives a GeometricFigure object. So it can calculate its own area by using the object's method.
public class AreaCalculator {
public double calculateArea(GeometricFigure figure){
return figure.calculateArea();
}
}
The library is used as follows:
public class App {
public static void main(String[] args) {
AreaCalculator areaCalc = new AreaCalculator();
Square square = new Square(3.15);
Rectangle rectangle = new Rectangle(7.85, 10.85);
Circle circle = new Circle(7.98);
System.out.println("Square's area: " + areaCalc.calculateArea(square));
System.out.println("Rectangle's area: " + areaCalc.calculateArea(rectangle));
System.out.println("Circle's area #1:" + areaCalc.calculateArea(circle));
}
}
The new design for the Area Calculator library allows us to extend the functionality of any geometric figure as long as we implement the GeometricFigure interface. Now the library complies with the Open/Closed Principle.
Should we always apply the Open/Closed Principle?
The OCP should not be an unbreakable rule when designing software solutions, it should be more like a best practice. The problem with OCP, as the same as the Single Responsibility Principle, is that you must guess the possible future changes your code will suffer. This can be easy for a senior or semi-senior developer, but for a junior developer could be a disaster. The poor implementation of the OCP could complicate the structure needlessly.
My recommendation is to always design your solutions in a way that your code is easily understood by other developers and easily scalable for changes in the future. About OCP, it is always better to wait for the first iteration of changes (because it always changes) so that you can identify the modules where you can apply the Open/Closed Principle.
If you like to read more about OCP, you can have a look at Uncle Bob's Blog.
In the next post, we will talk about the Liskov Substitution Principle.
Posted on April 8, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.