The open-closed principle asks us to provide a way to add new behavior without repeatedly changing the stable code that uses it. An interface can define the behavior that callers need, while separate implementations provide its variations.
A report generator is one example. It can ask a report writer to format some data without knowing whether that writer produces plain text or HTML. When we add another format, we add another writer instead of putting more formatting conditions into the generator.
OCP is one of the SOLID principles. Bertrand Meyer introduced the principle, commonly described as keeping software open for extension and closed for modification.
1. What Do Open and Closed Mean?
“Open for extension” means that we can add a supported variation. “Closed for modification” means that the stable part does not need to be rewritten for each variation.
In a report application, a new output format is a variation. Preparing the report and asking a writer to format it can remain stable.
An interface is a contract that describes operations an implementation must provide. Here, ReportWriter describes how to write a report. The generator uses that contract without choosing formatting rules itself.
OCP does not mean we can never edit existing code. We still fix defects, improve unclear code and change a contract when its assumptions no longer fit. We choose an extension point for changes that are useful or reasonably expected, rather than trying to predict every possible future requirement.
2. A Generator That Changes for Every Format
Our small report has a title and a count. The examples use Java 17 or later. A record holds these values and supplies the title() and count() accessors. The following version takes a string to choose a format and puts both formatting rules into one method.
record Report(String title, int count) {}
final class ReportGenerator {
String generate(Report report, String format) {
if ("text".equals(format)) {
return report.title() + "\nCount = " + report.count();
}
if ("html".equals(format)) {
return "<h1>" + report.title() + "</h1><p>Count = "
+ report.count() + "</p>";
}
throw new IllegalArgumentException("Unsupported format " + format);
}
}
When we add another format, we edit this method again. As the formats become more involved, the generator collects unrelated formatting details. This initial HTML branch also inserts the title without escaping it. The extracted HTML writer below corrects that separately from the design change.
An if or switch is not automatically a design mistake. A short condition over a small, fixed set of choices can be perfectly clear. We consider an extension point when the set keeps growing or when formatting behavior needs to evolve separately.
3. Give Each Format Its Own Writer
We introduce ReportWriter with one operation, write(). TextReportWriter produces plain text, while HtmlReportWriter produces HTML. The generator receives whichever writer the caller needs.
interface ReportWriter {
String write(Report report);
}
final class TextReportWriter implements ReportWriter {
@Override
public String write(Report report) {
return report.title() + "\nCount = " + report.count();
}
}
final class HtmlReportWriter implements ReportWriter {
@Override
public String write(Report report) {
return "<h1>" + escape(report.title()) + "</h1><p>Count = "
+ report.count() + "</p>";
}
private String escape(String value) {
return value.replace("&", "&")
.replace("<", "<")
.replace(">", ">");
}
}
final class ReportGenerator {
String generate(Report report, ReportWriter writer) {
return writer.write(report);
}
}
The HTML writer escapes special characters in the title before placing it in HTML text. This keeps a title containing < or & from being interpreted as HTML markup. A real template or reporting library can handle more complex rendering requirements.
The generator now delegates the formatting work. This uses composition, where an object works with another object, rather than requiring a subclass of the generator. The generator is deliberately small here so the extension point is visible. A larger application could also prepare shared report data before invoking the writer.
4. Add a Format Without Editing the Generator
Suppose we also need a short summary for a command-line display. We add another writer.
final class SummaryReportWriter implements ReportWriter {
@Override
public String write(Report report) {
return report.title() + " (" + report.count() + ")";
}
}
The caller can select it when building the application or handling a request.
Report report = new Report("Completed orders", 12);
ReportGenerator generator = new ReportGenerator();
String result = generator.generate(report, new SummaryReportWriter());
System.out.println(result);
The result is the following text.
Completed orders (12)
We added SummaryReportWriter without changing ReportGenerator, TextReportWriter or HtmlReportWriter. Some code must still select or register the new writer. OCP keeps that selection separate from the stable formatting caller; it does not eliminate application configuration.
5. What Should We Test?
We test each writer’s output separately. For example, the text writer needs a check for the title and count, while the HTML writer also needs a title containing special characters.
We then check that the generator delegates to a supplied writer. A simple test implementation can return a known string. That check does not prove that every real writer formats correctly, which is why the writer tests remain useful. The unit testing guide explains how to keep these checks focused.
The interface must also remain useful. If a new format needs information that Report does not contain, we need to reconsider the data contract. Adding a class cannot compensate for an abstraction that no longer describes the work.
6. Common Misunderstandings
- Extension does not require inheritance. We can supply another object that implements an interface, as this example does.
- Existing code is still editable. A defect or a changed requirement can justify modifying it.
- More interfaces do not automatically improve a design. A useful interface separates behavior that needs to vary.
- Testing still matters. A new implementation and its application configuration need checks.
The Java guide to using an interface as a type explains the language feature behind this approach.
7. Conclusion
We apply OCP by identifying a useful variation and giving it a clear contract. In this example, report formats vary, while the generator only asks a writer to produce a result. New formats can then be added without repeatedly changing that caller.
Happy Learning!
Hii Lokesh, i appreciate your hard work to put every article simple. I appreciate the way u explained all the design patterns putting your experience on design patterns in a simple clear way.
I am clear about Open/close principle.
I work on spring boot web based projects. I prefer module based packaging over layer based packing so all the packages contains classes only related to that module feature.
I keep no dependency between packages so that it helps me to change the class code of any package in future easily because there is no dependency between packages.
After reading open close principle i got a lot of doubts in me. I used to follow adding new feature directly to existing classes of controller, service and repository. I need your clarification on below points to clear my doubts
As u already know we write controllers, services and repositories separately, each class contains multiple methods. we don’t write a separate class which contain only single method which perform add operation and one more class which perform get operation to perform crud operations or business logics separately.
1. How can i follow open/close principle when i have a new requirement to add new functionality to the module feature do i need to write a separate controller class service class and spring data repository ?
Ex: user management module earlier i provided only add and get functionality and released to production. Know the requirement is to provide delete operation of user do i need to create a separete classes of all controller, service etc. Then extend it to previous controller, service class and add new functionality to newly created controller.
2. what i should do when a new requirement comes to provide delete functionality to delete user in user management module. How to take up this new requirement and write a code for all layers which follows open/close principle. so that my code neer breaks any other code.
3. What is the best way in spring MVC layer based application world to follow this Open/close Principle can u expain with exmpale in detail so it clarifies many developers.
can u expain in detail these above mentioned points with example.
Thank you in advance, I appreciate your work on sharing your knowledge wisdom.