There's a certain type of method that almost every developer has written at some point. It starts with a simple if statement. Then, when someone requests a new feature, so you add an else if. A few weeks later, another requirement comes in, and you add another condition. Six months later, you're staring at a method with fifteen conditions, a couple of nested switch statements, and a comment that says, "DO NOT TOUCH THIS CODE. EVERYTHING BREAKS WHEN YOU MODIFY IT."
Naturally, your next task is to modify that exact method. Welcome to software development!
The problem isn't that we used conditional statements. They're perfectly normal and useful. The problem appears when every new variation of a feature requires us to modify code that's already working. Sometimes, a small change introduces an unexpected bug in another part of the application. Other times, several developers end up modifying the same method for completely different requirements. And as more conditions are added, understanding the entire method becomes increasingly difficult.
This is the kind of problem the Open/Closed Principle (OCP) helps us address. It's the second principle in SOLID, and its central idea is simple: we should design software so that new behavior can be added without unnecessarily modifying existing, stable code.
Let's understand what that actually means, how to apply it, and when it makes sense to use it. We'll use pseudocode throughout the article, so you don't need to know any particular programming language.
What is the Open/Closed Principle?
The Open/Closed Principle states:
Software entities should be open for extension, but closed for modification.
The principle was introduced by software engineer Bertrand Meyer in his 1988 book Object-Oriented Software Construction.
At first, that definition sounds a little confusing. How can something be open and closed at the same time? The confusion disappears once we understand what those two words refer to.
Open for extension means we should be able to introduce new functionality or variations of existing behavior. For example, suppose an online shopping application provides discounts to Regular, Premium, and Student customers. If the company decides to introduce a discount for senior citizens, the application should be able to support it. We're extending the application's functionality.
Closed for modification means we should ideally be able to introduce that new discount without modifying the existing, working discount calculation code. The Regular, Premium, and Student discount implementations have already been developed and tested. Adding a new type of discount shouldn't require us to rewrite their logic or repeatedly modify the class responsible for using them.
In simpler terms, OCP encourages us to add new behavior by adding new code, rather than constantly rewriting code that already works.
This doesn't mean existing code must never change. We still need to fix bugs, improve implementations, and update existing business rules. The important distinction is between changing existing behavior and introducing new variations of behavior. OCP is particularly useful for the second case.
A simple real-world example
Think about an electrical socket in your home. You can connect a table lamp, a fan, or a phone charger to the same socket. The socket doesn't need to be redesigned every time you buy a new appliance. It provides a standard connection, and compatible devices are built to work with that connection.
The socket doesn't need to understand how a fan rotates, how a lamp produces light, or how a charger converts electricity. Each appliance handles its own functionality. As long as the device follows the required electrical and physical standards, the connection can remain unchanged.
Software can follow a similar idea. Instead of making one class understand every possible implementation, we can define a common contract that different implementations follow. The existing code works with that contract, while new implementations can be introduced without forcing the existing code to understand their internal details.
Of course, physical sockets and software abstractions aren't exactly the same thing. But the analogy helps explain why having a stable interface can make a system easier to extend.
Example 1: A discount calculator
Imagine you're developing an online shopping application. The store provides different discounts depending on the type of customer. Regular customers receive a 5% discount, Premium customers receive 10%, and Students receive 15%.
A product costs 100. After applying the discount, a Regular customer pays 95, a Premium customer pays 90, and a Student pays 85.
One simple way to implement this functionality is to create a class called DiscountCalculator and use conditional statements to select the appropriate discount.
CLASS DiscountCalculator
FUNCTION Calculate(customerType, price)
IF customerType == "Regular"
RETURN price * 0.95
ELSE IF customerType == "Premium"
RETURN price * 0.90
ELSE IF customerType == "Student"
RETURN price * 0.85
ELSE
THROW "Unknown customer type"
END IF
END FUNCTION
END CLASSWe can use the calculator like this:
calculator = NEW DiscountCalculator()
PRINT calculator.Calculate("Regular", 100)
PRINT calculator.Calculate("Premium", 100)
PRINT calculator.Calculate("Student", 100)Output:
95
90
85The code works, and honestly, there's nothing particularly wrong with this implementation for a small application. It's straightforward, readable, and easy to understand. If the company only has three discount types and those rules rarely change, this might be all we need.
But let's assume our online store is growing and the business frequently introduces new discount schemes.
Here comes the first change request
A few weeks later, the manager announces that senior citizens should receive a 20% discount. We open DiscountCalculator, find the existing conditions, and add another one.
ELSE IF customerType == "Senior"
RETURN price * 0.80The new discount works. The manager is happy, and we move on to another feature.
Next month, the company introduces an employee discount. We return to the same class and add another condition. Then comes a holiday discount, followed by a loyalty discount. Before long, our discount calculator is becoming a collection of discount rules held together by else if statements.
And that's still the simple version. Real-world discounts can be considerably more complicated. A Student discount might require checking eligibility. A Premium discount could depend on the purchase amount, while a holiday discount might apply only to certain products. Some discounts might be combined, while others must never be used together.
Now our calculator isn't simply choosing a percentage. It's slowly becoming responsible for implementing every possible discount rule in the application.
Why is this a problem?
The main issue is that DiscountCalculator needs to know about every discount type. Whenever a new discount is introduced, we have to modify the same class. That means opening code that's already working and changing its implementation to accommodate another variation.
Suppose the existing discounts have been in production for six months. They've been tested, and customers are using them every day. Now we add the Senior discount. A small mistake in the new condition might affect an existing rule, especially if the conditions become more complicated or start overlapping. The more frequently we modify the same method, the more opportunities we create for accidental regressions.
There's also the maintenance problem. Imagine two developers working on separate tickets: one is introducing an employee discount, while another is adding a loyalty discount. Both need to modify the same method. Depending on the changes, they might end up with a merge conflict even though they're implementing completely separate business rules.
What if DiscountCalculator didn't need to know about individual customer types at all? What if each discount could calculate its own result, while the calculator simply used whichever discount was supplied to it?
That's the idea we'll explore next.
Applying the Open/Closed Principle
Instead of putting every discount rule inside DiscountCalculator, we'll move those rules into separate implementations. Regular, Premium, Student, and Senior discounts will each know how to calculate their own final price.
But we still need the calculator to work with all these implementations. We need something they have in common, and that's where abstraction comes in.
An abstraction describes what something can do without necessarily describing how it does it. In this example, every discount should accept a price and return the final price after applying its discount. We can express that behavior using an interface.
Step 1: Define a common contract
INTERFACE Discount
FUNCTION Apply(price)
END INTERFACEThink of an interface as an agreement. Any class implementing Discount promises to provide an Apply() function. The interface doesn't decide whether the discount is 5%, 10%, or 20%. That's up to the individual implementation.
Different programming languages provide different ways to express this contract. C# and Java have interfaces, Python supports protocols and abstract base classes, and other languages may use traits, function types, or conventions. The implementation details differ, but the design idea is the same.
Step 2: Create the discount implementations
Now let's create separate classes for our existing discounts.
CLASS RegularDiscount IMPLEMENTS Discount
FUNCTION Apply(price)
RETURN price * 0.95
END FUNCTION
END CLASS
CLASS PremiumDiscount IMPLEMENTS Discount
FUNCTION Apply(price)
RETURN price * 0.90
END FUNCTION
END CLASS
CLASS StudentDiscount IMPLEMENTS Discount
FUNCTION Apply(price)
RETURN price * 0.85
END FUNCTION
END CLASSEach class now owns its own discount calculation. If the company changes the Premium discount from 10% to 12%, the change belongs in PremiumDiscount. There's no need to modify the Student or Regular discount implementations.
More importantly, the individual discount rules no longer need to live inside the calculator. We've separated the different behaviors and given them a common contract.
Step 3: Make the calculator use the abstraction
Let's rewrite our DiscountCalculator.
CLASS DiscountCalculator
FUNCTION Calculate(discount, price)
RETURN discount.Apply(price)
END FUNCTION
END CLASSThat's it. No customer type checks, no long chain of conditions, and no knowledge of individual discount percentages.
The calculator receives an object that follows the Discount contract and asks it to calculate the final price. It doesn't care whether the object represents a Regular, Premium, or Student discount.
Let's use it:
calculator = NEW DiscountCalculator()
regular = NEW RegularDiscount()
premium = NEW PremiumDiscount()
student = NEW StudentDiscount()
PRINT calculator.Calculate(regular, 100)
PRINT calculator.Calculate(premium, 100)
PRINT calculator.Calculate(student, 100)Output:
95
90
85The application produces exactly the same results as before, but we've changed how responsibilities are organized. Previously, DiscountCalculator knew about every customer type and the rules associated with it. Now the calculator knows only about the Discount contract.
That small change in design makes a big difference when we introduce another discount.
Step 4: Add the Senior discount
Now imagine the manager returns with the senior citizen discount requirement. This time, instead of opening DiscountCalculator, we create a new class.
CLASS SeniorDiscount IMPLEMENTS Discount
FUNCTION Apply(price)
RETURN price * 0.80
END FUNCTION
END CLASSWe can immediately use it:
senior = NEW SeniorDiscount()
PRINT calculator.Calculate(senior, 100)Output:
80We didn't modify DiscountCalculator, RegularDiscount, PremiumDiscount, or StudentDiscount. We simply introduced a new implementation of the existing contract.
We extended the application's behavior without modifying the existing calculator. That's the Open/Closed Principle in action.
Understanding the pattern behind OCP
Let's look at what actually changed during the refactoring.
Initially, DiscountCalculator contained a growing collection of conditions. Each condition represented a different version of the same general behavior: calculating a discount. Whenever another version was introduced, we modified the calculator.
After refactoring, the different behaviors became independent implementations. The calculator no longer needs to know their names or internal rules. It only needs to know that each implementation supports Apply().
This approach is closely related to polymorphism.
Polymorphism allows different objects to respond to the same operation using their own implementations. In our example, all discount classes provide an Apply() function, but they calculate the final price differently. When the calculator calls Apply(), the actual discount object determines which implementation runs.
This is one of the most common ways developers apply OCP in object-oriented programming. Instead of making an existing class recognize every new type, we let new implementations provide their own behavior through a common abstraction.
But polymorphism isn't the only way to follow OCP. Functions, callbacks, plugins, configuration, and other extension mechanisms can also help. The technique we choose depends on the kind of changes our application needs to support.
Wait, don't we still need to select the discount?
Yes, and this is an important detail that some OCP explanations skip.
Our calculator now works with any discount implementation, but the application still needs to decide which discount should be applied.
For example, we might have code like this:
IF customer.type == "Regular"
discount = NEW RegularDiscount()
ELSE IF customer.type == "Premium"
discount = NEW PremiumDiscount()
ELSE IF customer.type == "Student"
discount = NEW StudentDiscount()
END IFSo haven't we just moved the conditional statements somewhere else?
In this example, yes. But the responsibilities are now different. The selection code decides which discount to use, while DiscountCalculator applies the selected implementation. Adding a new discount doesn't require modifying the calculator itself.
If new discounts are introduced frequently, we might also want to avoid repeatedly editing the selection logic. One option is to use a registry, which maps discount identifiers to their implementations.
discounts = {
"Regular": RegularDiscount,
"Premium": PremiumDiscount,
"Student": StudentDiscount
}When a customer selects a discount, the application looks up the appropriate implementation.
discountType = customer.selectedDiscount
discountClass = discounts[discountType]
discount = NEW discountClass()
finalPrice = calculator.Calculate(
discount,
originalPrice
)When we introduce the Senior discount, we can register it:
discounts["Senior"] = SeniorDiscountSome applications use dependency injection or plugin discovery to register implementations automatically. Others use a configuration file or a database table.
But don't rush to introduce a registry or factory just because it sounds more professional. If your application has three fixed discount types, a simple conditional statement might be easier to understand and maintain.
OCP doesn't mean that adding a feature will never require changing a single line anywhere in the application. It means we can design stable parts of our system so that new behavior can be introduced without modifying their implementation.
Our example also assumes that one discount is selected at a time. Real shopping applications may support combining discounts, eligibility checks, minimum purchase requirements, or rules about which discount takes priority. Those requirements need to be considered separately rather than assuming one simple interface automatically solves every pricing problem.
Example 2: Calculating the area of different shapes
Let's look at another simple example.
Suppose you're developing an application that calculates the total area of different shapes. Initially, the application supports circles and rectangles.
A straightforward implementation might look like this:
CLASS AreaCalculator
FUNCTION TotalArea(shapes)
total = 0
FOR EACH shape IN shapes
IF shape.type == "Circle"
total = total +
(3.14 * shape.radius * shape.radius)
ELSE IF shape.type == "Rectangle"
total = total +
(shape.width * shape.height)
END IF
END FOR
RETURN total
END FUNCTION
END CLASSThe code works, but notice that AreaCalculator needs to understand how every supported shape calculates its area. If we introduce triangles, we need to modify the calculator. If we introduce more shapes later, the calculator keeps growing.
Instead, we can define a common Shape contract.
INTERFACE Shape
FUNCTION Area()
END INTERFACENow each shape can provide its own implementation.
CLASS Circle IMPLEMENTS Shape
radius
FUNCTION Area()
RETURN 3.14 * radius * radius
END FUNCTION
END CLASS
CLASS Rectangle IMPLEMENTS Shape
width
height
FUNCTION Area()
RETURN width * height
END FUNCTION
END CLASSOur area calculator becomes much simpler.
CLASS AreaCalculator
FUNCTION TotalArea(shapes)
total = 0
FOR EACH shape IN shapes
total = total + shape.Area()
END FOR
RETURN total
END FUNCTION
END CLASSThe calculator no longer checks whether a shape is a circle or rectangle. It simply calls the Area() function provided by each shape.
Now suppose we need to introduce triangles.
CLASS Triangle IMPLEMENTS Shape
base
height
FUNCTION Area()
RETURN 0.5 * base * height
END FUNCTION
END CLASSWe can use our new triangle without modifying AreaCalculator.
shapes = [
NEW Circle(radius = 2),
NEW Rectangle(width = 4, height = 5),
NEW Triangle(base = 6, height = 3)
]
calculator = NEW AreaCalculator()
PRINT calculator.TotalArea(shapes)The result is 41.56, using 3.14 as our simplified value of pi.
The important part isn't the calculation itself. It's that AreaCalculator can work with any new shape that follows the Shape contract.
This is another example of using polymorphism to support OCP.
Example 3: Exporting reports
Let's consider a feature that appears in many business applications: exporting reports.
Suppose an application initially supports PDF exports. Later, customers request CSV and Excel formats.
We might start with a ReportService that contains conditions for every format.
CLASS ReportService
FUNCTION Export(report, format)
IF format == "PDF"
GENERATE PDF
ELSE IF format == "CSV"
GENERATE CSV
ELSE IF format == "Excel"
GENERATE EXCEL
END IF
END FUNCTION
END CLASSEvery new export format requires another condition, and the service ends up containing implementation details for several unrelated file formats.
If the application is expected to support many export formats, a common exporter contract could help.
INTERFACE ReportExporter
FUNCTION Export(report)
END INTERFACEEach format gets its own implementation.
CLASS PdfExporter IMPLEMENTS ReportExporter
FUNCTION Export(report)
GENERATE PDF FILE
END FUNCTION
END CLASS
CLASS CsvExporter IMPLEMENTS ReportExporter
FUNCTION Export(report)
GENERATE CSV FILE
END FUNCTION
END CLASSThe service works with the supplied exporter.
CLASS ReportService
FUNCTION ExportReport(report, exporter)
exporter.Export(report)
END FUNCTION
END CLASSWhen we need Excel support, we can introduce ExcelExporter without modifying the stable report service or the existing exporters.
This example also shows how SRP and OCP can work together. Each exporter owns the logic for its particular format, while the report service can work with different implementations through a common abstraction.
Who introduced the Open/Closed Principle?
The idea behind OCP has been around much longer than modern web frameworks and microservices. Its history also helps explain why developers commonly associate it with inheritance, abstractions, and design patterns.
Bertrand Meyer: The original principle
Software engineer Bertrand Meyer introduced the Open/Closed Principle in his 1988 book Object-Oriented Software Construction.
Meyer was concerned with building reusable software modules. Imagine you've written a useful class, tested it, and started using it throughout an application. Other developers may also depend on that class. If you repeatedly modify it whenever new functionality is requested, you risk affecting the code that already depends on it.
At the same time, software needs to evolve. New requirements will always appear, and refusing to extend existing functionality isn't a realistic option.
Meyer's principle addressed this problem by encouraging modules that could be extended while remaining stable for existing users. His original approach placed considerable emphasis on inheritance, where new classes could extend existing ones without changing the original implementation.
Robert C. Martin: Abstraction and polymorphism
Robert C. Martin, commonly known as Uncle Bob, later helped popularize the Open/Closed Principle through his writing and teaching.
In his 1996 article The Open-Closed Principle, Martin discussed how abstractions and polymorphism can help developers introduce new functionality while protecting stable parts of an application.
This is closely related to our discount calculator example. Instead of making DiscountCalculator directly depend on every discount implementation, we introduced a common Discount contract. The calculator works through that abstraction, while individual classes provide the actual discount behavior.
Martin also discusses OCP in Clean Architecture: A Craftsman's Guide to Software Structure and Design. There, the idea applies beyond individual classes. It can influence how larger software components are organized so that changes in one part of the system don't unnecessarily affect other parts.
The Gang of Four and the Strategy Pattern
Another influential contribution comes from the book Design Patterns: Elements of Reusable Object-Oriented Software, written by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. These authors are commonly known as the Gang of Four, or GoF.
One of the book's well-known design guidelines is:
Program to an interface, not an implementation.
This fits naturally with OCP. Instead of depending directly on one specific implementation, code can depend on a common contract that multiple implementations follow.
The book also introduces the Strategy Pattern, which allows different algorithms or behaviors to be implemented separately and used interchangeably. Our discount calculator resembles a Strategy design: Regular, Premium, Student, and Senior discounts are different strategies for calculating the final price.
OCP is the principle we're trying to follow. Strategy is one possible technique for achieving it. The distinction matters because not every OCP problem requires the Strategy Pattern.
Martin Fowler: Refactor when the need becomes clear
Martin Fowler's book Refactoring: Improving the Design of Existing Code is another useful resource when learning these ideas.
We don't always know which parts of an application will need to support new variations. Perhaps an online store begins with only two discount types, and a simple conditional statement works perfectly well. Later, more discount types are introduced, and we repeatedly modify the same method.
At that point, we have a clearer understanding of how the application is changing. We can refactor the code by introducing a common abstraction, moving the discount rules into separate implementations, and using tests to verify that the existing behavior still works.
This is often a more practical approach than creating interfaces and design patterns for every hypothetical requirement before the application has even been released.
Does OCP mean existing code should never change?
No, and this is probably the most important misunderstanding to avoid.
Suppose the company currently provides a 10% discount to Premium customers. The manager decides to increase it to 12%. We would naturally modify the PremiumDiscount implementation because the business rule for that particular discount has changed.
Similarly, if we discover a bug in an existing implementation, we should fix it. OCP doesn't suggest that code should remain broken because it's supposedly closed for modification.
The difference is between changing an existing rule and introducing a new variation. Changing the Premium discount from 10% to 12% affects an existing rule. Introducing a Senior discount adds another rule. A suitable OCP design allows us to add the new variation without rewriting the calculator or the other discount implementations.
It's also worth remembering that no abstraction can predict every possible future requirement. Sometimes the shared contract itself may need to change because the system has fundamentally evolved. Being closed for modification is a useful design goal for expected extensions, not a permanent guarantee that a class will never need editing.
Does every if-else statement violate OCP?
Absolutely not.
Conditional statements are normal programming tools, and trying to replace every condition with a class hierarchy would make many programs unnecessarily complicated.
Consider a simple function that checks whether a number is positive.
FUNCTION IsPositive(number)
IF number > 0
RETURN TRUE
ELSE
RETURN FALSE
END IF
END FUNCTIONThere's no obvious reason to create separate PositiveNumberStrategy and NegativeNumberStrategy classes for this calculation.
Similarly, consider a password validation function.
FUNCTION ValidatePassword(password)
IF LENGTH(password) < 8
RETURN "Password is too short"
END IF
RETURN "Valid"
END FUNCTIONFor this simplified validation requirement, the condition is perfectly reasonable. The code is easy to understand, and introducing additional abstractions wouldn't necessarily improve anything.
OCP becomes more useful when we have different implementations of the same general behavior that are expected to grow or change independently. Examples include payment methods, notification channels, export formats, and discount strategies.
A long conditional chain isn't automatically a problem, but if you repeatedly modify the same chain every time a new implementation is introduced, it's worth asking whether the design needs a better extension point.
Can we take OCP too far?
Definitely. This is where software design can become a little entertaining.
Imagine you're developing a small application that supports exactly two discounts. The requirements are stable, and there's no indication that the business plans to introduce more.
But you've recently learned about OCP, so you decide to create an elaborate architecture.
Discount
DiscountStrategy
DiscountStrategyFactory
DiscountStrategyRegistry
DiscountStrategyResolver
DiscountStrategyConfiguration
DiscountStrategyManagerNow applying a 5% discount requires navigating through seven different files. The application is technically extensible, but understanding how it works has become much harder than necessary.
Every abstraction has a cost. Interfaces, factories, registries, and additional classes can make software more flexible, but they also introduce more relationships and concepts that developers need to understand.
The goal of OCP isn't to make every part of an application infinitely extensible. It's to make expected changes easier and safer.
A practical approach is to start with a simple design and pay attention to how requirements evolve. If a second or third variation reveals that the same code keeps changing, consider whether introducing an abstraction would help. There's no magical rule that says you must wait for exactly three implementations, though. If the requirements already establish that many interchangeable behaviors will be needed, designing an extension point from the beginning may be sensible.
The important thing is to understand why you're introducing the abstraction. If it solves a real problem, great. If it exists only because a design principle sounds impressive, you might be making life harder for whoever has to maintain the code later.
And that person might be you.
OCP doesn't always require classes
So far, we've used interfaces and classes because they make the principle easy to demonstrate. But the underlying idea doesn't require a large class hierarchy.
In languages that support passing functions as values, we could make the discount calculator accept a function instead of an object.
FUNCTION CalculateDiscount(discountFunction, price)
RETURN discountFunction(price)
END FUNCTIONNow we can define discount functions independently.
FUNCTION RegularDiscount(price)
RETURN price * 0.95
END FUNCTION
FUNCTION StudentDiscount(price)
RETURN price * 0.85
END FUNCTIONAnd pass them to the calculator.
PRINT CalculateDiscount(RegularDiscount, 100)
PRINT CalculateDiscount(StudentDiscount, 100)Output:
95
85If we introduce another discount, we can define another compatible function without modifying CalculateDiscount.
This demonstrates that OCP is about designing extension points, not about using one particular object-oriented feature. Depending on the application, interfaces, functions, callbacks, plugins, or configuration-driven rules may all be suitable approaches.
Sometimes the variation doesn't even need a new class or function. If all discounts are simple percentages, storing the discount percentages in a database or configuration file may be more practical than creating a separate class for every percentage.
The design should match the type of change we're actually trying to support.
How OCP helps with testing
One advantage of separating discount implementations is that we can test them independently.
For example, we can test Student Discount without involving other discount types.
TEST "Student discount applies 15%"
discount = NEW StudentDiscount()
result = discount.Apply(100)
ASSERT result == 85
END TESTSimilarly, we can test Premium Discount.
TEST "Premium discount applies 10%"
discount = NEW PremiumDiscount()
result = discount.Apply(100)
ASSERT result == 90
END TESTWhen the company introduces Senior Discount, we add another test for the new implementation.
TEST "Senior discount applies 20%"
discount = NEW SeniorDiscount()
result = discount.Apply(100)
ASSERT result == 80
END TESTThe existing tests don't need to be rewritten simply because another discount type has been introduced. We should still run the complete test suite, of course, because new code can introduce integration problems even when stable components remain unchanged.
The benefit is that the new behavior is more isolated. We can verify its calculation independently and reduce the number of existing components that need implementation changes.
We can also test the calculator itself using a simple test implementation.
CLASS TestDiscount IMPLEMENTS Discount
FUNCTION Apply(price)
RETURN 42
END FUNCTION
END CLASSThen write a test:
TEST "Calculator uses the supplied discount"
calculator = NEW DiscountCalculator()
discount = NEW TestDiscount()
result = calculator.Calculate(discount, 100)
ASSERT result == 42
END TESTThis verifies that the calculator delegates the calculation to the supplied implementation rather than trying to determine the discount itself.
How OCP relates to the other SOLID principles
The SOLID principles often work together, although each focuses on a different design concern.
The Single Responsibility Principle (SRP) encourages us to keep responsibilities that change for unrelated reasons separate. In our discount example, each discount implementation owns its own calculation rules. Changes to one type of discount don't require modifying the implementation of another.
The Open/Closed Principle (OCP) takes this further by making the calculator work through a shared contract. This allows new discount implementations to be introduced without changing the stable calculator.
The Liskov Substitution Principle (LSP) becomes relevant because every implementation should follow the expectations of the shared contract. If Discount.Apply() is expected to return a valid final price, a new implementation shouldn't unexpectedly perform unrelated operations or return invalid values.
The Interface Segregation Principle (ISP) encourages us to avoid creating interfaces that force implementations to support operations they don't need. And the Dependency Inversion Principle (DIP) encourages higher-level components to depend on suitable abstractions rather than tightly coupling themselves to specific lower-level implementations.
These principles complement one another, but that doesn't mean every class needs to demonstrate all five. It's more useful to understand which design problem you're trying to solve and apply the relevant principle where it provides a real benefit.
A practical checklist for applying OCP
When reviewing a feature, I find it useful to ask a few questions before introducing an abstraction:
- Does this feature support multiple versions of the same behavior?
- Are new versions likely to be introduced regularly?
- Does introducing a new version require modifying the same existing method?
- Can the implementations follow a meaningful common contract?
- Can new behavior be tested without changing tests for existing behavior?
- Will the abstraction make the application easier to maintain, or simply add more layers?
These questions aren't strict rules. They're a way to decide whether an extension point is worth introducing.
Sometimes a simple conditional statement is enough. Other times, a common interface or another extension mechanism can make future changes much easier.
The decision should depend on the requirements and expected changes, not simply the number of if statements in the code.
Final thoughts
The Open/Closed Principle is based on something every developer eventually learns: software requirements change. A feature that supports three options today might support ten next year, and those new options can introduce new business rules, integrations, and edge cases.
If every new variation forces us to modify the same stable code, the application can become harder to maintain over time. OCP encourages us to introduce useful extension points so that new behavior can be added without repeatedly disturbing existing implementations.
Our original discount calculator contained conditions for Regular, Premium, and Student discounts. Every new discount required another condition. After introducing a common Discount contract, we could add Senior Discount as a separate implementation without modifying the calculator. The same idea worked for shapes and report exporters.
But OCP isn't about replacing every conditional statement with polymorphism, creating interfaces for everything, or refusing to modify existing code. It's about identifying the parts of a system that are likely to gain new variations and giving those parts a sensible way to grow.
So the next time you're adding another else if to a method that's already becoming difficult to maintain, ask yourself:
"Am I adding a new behavior, or am I forcing an existing class to understand yet another special case?"
If it's the second one, you may have found a good place to apply the Open/Closed Principle.
And perhaps it's finally time to remove that comment saying, "DO NOT TOUCH THIS CODE."
Practice questions
Question 1: What does the Open/Closed Principle state?
Answer: The Open/Closed Principle states that software entities should be open for extension but closed for modification. This means we should be able to introduce new behavior without unnecessarily changing existing, stable code.
Question 2: Who introduced the Open/Closed Principle?
Answer: Bertrand Meyer introduced the principle in his 1988 book Object-Oriented Software Construction. Robert C. Martin later helped popularize approaches using abstractions and polymorphism.
Question 3: What's the problem with this code?
CLASS PaymentProcessor
FUNCTION Process(paymentType)
IF paymentType == "Card"
PROCESS CARD PAYMENT
ELSE IF paymentType == "UPI"
PROCESS UPI PAYMENT
END IF
END FUNCTION
END CLASSAnswer: If the application is expected to support many payment methods, the class will need to be modified whenever another payment type is introduced. A possible improvement is to define a common payment contract and create separate implementations for Card, UPI, and other payment methods. The processor could then work through that contract.
Question 4: Does OCP mean existing code should never change?
Answer: No. Existing code can still be modified to fix bugs, improve performance, or update existing business rules. OCP encourages us to introduce new variations without unnecessarily modifying stable components.
Question 5: Explain polymorphism in simple words.
Answer: Polymorphism allows different objects to respond to the same operation using their own implementations. For example, RegularDiscount and StudentDiscount both provide Apply(), but each calculates a different discount. The calculator can use either implementation without knowing its internal details.
Question 6: Does every conditional statement violate OCP?
Answer: No. Conditional statements are normal programming tools. A simple condition that checks a fixed rule doesn't need a class hierarchy. OCP is more useful when the same component repeatedly needs to change to support new variations of behavior.
Question 7: How would you apply OCP to a report export feature?
Answer: We could define a common ReportExporter contract and create separate implementations for PDF, CSV, and Excel. The export service would use the supplied implementation through the shared contract. Adding another format would then involve creating a new exporter rather than modifying the stable service.
Question 8: What's the difference between OCP and the Strategy Pattern?
Answer: OCP is a software design principle that encourages extension without unnecessary modification. The Strategy Pattern is a design pattern that allows different algorithms or behaviors to be implemented separately and used interchangeably. Strategy can help achieve OCP, but it isn't the only approach.
Question 9: Why shouldn't we introduce interfaces everywhere?
Answer: Interfaces and abstractions add complexity. If a feature has one simple, stable behavior and no meaningful need for alternative implementations, an interface may provide little value. It's better to introduce abstractions when they solve a real problem or support reasonably expected changes.
Question 10: Explain the Open/Closed Principle in your own words.
Sample answer: The Open/Closed Principle means designing software so that we can add new features or variations without repeatedly modifying code that already works. For example, if an online store supports several discount types, we can define a common discount contract and create a separate implementation for each discount. When a new discount is introduced, we add another implementation instead of modifying the existing calculator.
Recommended reading
Bertrand Meyer — *Object-Oriented Software Construction*. The foundational work associated with the Open/Closed Principle. It discusses modularity, reuse, and extending software through inheritance.
Robert C. Martin — *The Open-Closed Principle* (1996). Discusses using abstractions and polymorphism to create software that can be extended while protecting stable modules.
Robert C. Martin — *Clean Architecture: A Craftsman's Guide to Software Structure and Design*. Explores OCP at an architectural level and explains how system components can be organized to reduce the impact of changes.
Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides — *Design Patterns: Elements of Reusable Object-Oriented Software*. Introduces patterns such as Strategy that help developers create interchangeable implementations.
Martin Fowler — *Refactoring: Improving the Design of Existing Code*. Explains how to improve existing software designs when requirements reveal the need for better abstractions.