The principle states that software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. This means you should be able to add new functionality to an existing class without changing its existing code.
Example of OCP Violation
Let’s consider a simple example where we have a Shape
class with a Draw
method. We want to support drawing different types of shapes, such as circles and rectangles.
The following example violates OCP:
|
|
In this example, if we want to add a new shape, we need to modify the Shape
class and the Draw
method. This violates the Open/Closed Principle because the class isn’t closed for modification.
Fixing the Violation With Abstract and Derived Classes
To fix this violation, we can use polymorphism and abstract classes/interfaces to ensure that we can extend the functionality without modifying existing code.
|
|
Now, if we want to add a new shape, such as a Triangle
, we can do so without modifying the existing Shape
class or the Circle
and Rectangle
classes.
|
|
Usage With Inheritance
Here’s how you can use the shapes:
|
|
In this revised example, the Shape
class is open for extension (you can add new shapes by inheriting from it) but closed for modification (you don’t need to change existing shape classes to add new functionality). This adheres to the Open/Closed Principle.
Alternatives to Inheritance
Now, inheritance isn’t the single option. Here’s how the Shape abstraction and derived classes could be reimagined using other methods for implementing the Open/Closed Principle (OCP).
Using Interfaces, Strategy Pattern and Dependency Injection
Replace the abstract base class with an interface:
|
|
Then, we have the concrete classes to implement the interface:
|
|
Finally, using dependency injection, we can use the shape classes:
|
|
In the program, we instantiate the shapes and provide them the ShapeBuilder
constructor:
|
|
With this structure, adding a new shape type (e.g., Hexagonal) only requires creating a new class that implements IShape
, without modifying ShapBuilder
, since we’ll provide it in the list of shapes that its constructor accepts.
But when should we use Abstract over Interface? Well, we could use both at the same time and that’ll depend on the project’s complexity.
Create base abstract shape class only when you have duplicated code in several IShape implementors, e.g., Rectangle
and Square
shapes would fall into that scenario.
When you create an abstract class (place where you move duplicated code), you shouldn’t change interface, because contract stays the same. Just inherit your base class from the original IShape interface.
Conclusion
We reviewed the Open/Closed Principle with some basic examples and different ways to solve the problem. The one you pick will depend on your business case.
Also, you may have noticed we also use the Dependency Inversion Principle (the “D” of S.O.L.I.D) and the Single Responsibility Principle (the “S” of S.O.L.I.D). Often, several principles will go together and today’s article highlights that fact.
But, remember, like a Tech Lead once said to me:
Think about the “What” first to pick the appropriate “How” afterwards.
Follow me
Thanks for reading this article. Make sure to follow me on X, subscribe to my Substack publication and bookmark my blog to read more in the future.
Photo by Athena Sandrini.