The Open/Closed Principle (OCP) is one of the foundational principles of object-oriented programming, coined by Bertrand Meyer. It states:
“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 a module or class without changing its existing code. This principle promotes maintainability, reduces the risk of introducing bugs when extending functionality, and adheres to good design practices.
Let us discussed this with some examples.
Invoice Example
Imagine you are designing a system to calculate and generate invoices. Here’s how applying or violating the Open/Closed Principle impacts the design.
Initial Design (Violating OCP)
class Invoice: def calculate_total(self, items): total = 0 for item in items: total += item.price return total
This simple class calculates the total of items. But now, let’s say you need to introduce discounts:
class Invoice: def calculate_total(self, items, discount_type=None): total = 0 for item in items: total += item.price if discount_type == "seasonal": total *= 0.9 # 10% discount elif discount_type == "clearance": total *= 0.8 # 20% discount return total
While it works, it violates the Open/Closed Principle because adding new discount types (e.g., “loyalty”) requires modifying the calculate_total method, increasing the risk of bugs and making the code less maintainable.
Improved Design (Adhering to OCP)
We refactor the code to allow extensions through inheritance or strategy patterns without modifying the existing class.
class DiscountStrategy: def apply_discount(self, total): return total # Default: no discount class SeasonalDiscount(DiscountStrategy): def apply_discount(self, total): return total * 0.9 # 10% discount class ClearanceDiscount(DiscountStrategy): def apply_discount(self, total): return total * 0.8 # 20% discount class Invoice: def __init__(self, discount_strategy: DiscountStrategy): self.discount_strategy = discount_strategy def calculate_total(self, items): total = sum(item.price for item in items) return self.discount_strategy.apply_discount(total)
Now, introducing a new discount (e.g., “LoyaltyDiscount”) requires creating a new subclass of DiscountStrategy without altering the Invoice class.
classDiagram class Invoice { +calculate_total(items: List[Item]): float -discount_strategy: DiscountStrategy } class DiscountStrategy { <<interface>> +apply_discount(total: float): float } class SeasonalDiscount { +apply_discount(total: float): float } class ClearanceDiscount { +apply_discount(total: float): float } class Item { +price: float } Invoice --> DiscountStrategy : uses DiscountStrategy <|-- SeasonalDiscount DiscountStrategy <|-- ClearanceDiscount Invoice --> Item : aggregates
You can add new discount types without changing the Invoice class, ensuring that existing functionality remains unaffected.
Conclusion
We can conclude the OCP discussion as follows:
- A software system should be easy to extend without requiring modifications to the existing system. The Open/Closed Principle (OCP) helps achieve this goal.
- The system must be divided into small components, arranged so that core code is always protected from new code.