Understanding Open-Close Principle
The Open-Close Principle (OCP) is one of the SOLID principles of object-oriented design. It was introduced by Bertrand Meyer in his book “Object-Oriented Software Construction” in 1988.
The principle states:
Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.
What does it mean?
- Open for Extension: We should be able to extend the behavior of a class without modifying its source code. This is achieved through inheritance or interfaces.
- Closed for Modification: Once a class has been written and tested, it should not be modified. Instead, we should add new functionality by extending the class.
Benefits of Open-Close Principle
-
Flexibility: The Open-Close Principle allows for easy modification and extension of existing code without the need to change the original code. This makes the system more adaptable to changes and easier to maintain.
-
Reusability: By adhering to the Open-Close Principle, we can reuse existing code and extend it to create new functionality, rather than writing new code from scratch. This saves time and reduces the risk of errors.
-
Testability: The Open-Close Principle makes the code more testable. By isolating the behavior we want to test, we can ensure that it works as expected and that any changes to the code do not affect the test results.
Example which violates Open-Close Principle
Consider a shape-drawing system that initially supports drawing rectangles. Now, if you want to add support for drawing circles, you’d have to modify the ShapeDrawer class, which violates the Open/Closed Principle.
class ShapeDrawer {
drawRectangle(width: number, height: number) {
console.log(`Drawing a rectangle with width ${width} and height ${height}`);
}
}
const drawer = new ShapeDrawer();
drawer.drawRectangle(10, 20);
Example which follows Open-Close Principle
To follow the OCP, we can use an interface for shapes and make the ShapeDrawer class work with any shape, which will allow us to add new shapes without modifying the class.
// Define a Shape interface
interface Shape {
draw(): void;
}
// Implement Rectangle shape
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}
draw() {
console.log(`Drawing a rectangle with width ${this.width} and height ${this.height}`);
}
}
// Implement Circle shape
class Circle implements Shape {
constructor(private radius: number) {}
draw() {
console.log(`Drawing a circle with radius ${this.radius}`);
}
}
// ShapeDrawer doesn't need to know the specific type of shapes
class ShapeDrawer {
drawShape(shape: Shape) {
shape.draw();
}
}
// Usage
const drawer = new ShapeDrawer();
const rectangle = new Rectangle(10, 20);
const circle = new Circle(15);
drawer.drawShape(rectangle);
drawer.drawShape(circle);
Explanation:
- The Shape interface defines a contract (draw() method) that all shapes must follow.
- The Rectangle and Circle classes implement the Shape interface.
- The ShapeDrawer class is now closed for modification (you don’t need to change it to add new shapes) but open for extension (you can add new shapes - like Circle, Triangle, etc., by implementing the Shape interface).
By following this approach, you adhere to the Open/Closed Principle, ensuring that your system can be extended with new functionality without modifying existing, stable code.