Introduction
The Open/Closed Principle (OCP) is the second letter in the SOLID principles of object-oriented design. It states:
Software entities should be open for extension, but closed for modification.
In other words, you should be able to add new functionality to a class or module without changing its existing code. This is especially important for systems that evolve over time or are maintained by large teams.
Let’s look at how to apply the Open/Closed Principle in real Java projects.
1. Recognizing the Violation
Suppose you are building a simple invoicing system:
class InvoicePrinter {
public void printInvoice(Invoice invoice, String format) {
if ("PDF".equals(format)) {
// PDF logic
} else if ("HTML".equals(format)) {
// HTML logic
}
}
}
This class is not closed for modification. Every time you need a new format (e.g., CSV), you must edit this class. That violates OCP.
2. Applying OCP with Polymorphism
A better design is to rely on interfaces or abstract classes, allowing you to add new behaviors through extension.
✅ Step 1: Define an abstraction
interface InvoiceFormatter {
void format(Invoice invoice);
}
✅ Step 2: Create concrete implementations
class PdfInvoiceFormatter implements InvoiceFormatter {
public void format(Invoice invoice) {
System.out.println("Formatting as PDF");
}
}
class HtmlInvoiceFormatter implements InvoiceFormatter {
public void format(Invoice invoice) {
System.out.println("Formatting as HTML");
}
}
✅ Step 3: Refactor the printer
class InvoicePrinter {
public void print(Invoice invoice, InvoiceFormatter formatter) {
formatter.format(invoice);
}
}
Now you can add new formats without modifying InvoicePrinter
.
3. Benefits of OCP
- Extensible systems: Easily add features with minimal risk of breaking things.
- Isolated testing: Each formatter can be tested independently.
- Pluggable architecture: Useful in plugin systems, payment gateways, UI components.
4. Real-World Use Case: Payment Methods
The Open/Closed Principle is especially valuable in domains where new features or integrations are frequently added. One common example is payment processing in e-commerce systems.
Let’s say you initially support only credit card payments:
class CheckoutService {
public void pay(Payment payment) {
System.out.println("Processing credit card...");
}
}
Soon, your business needs to support PayPal, Stripe, and others. If you modify CheckoutService
each time, the class becomes fragile and tightly coupled to all payment logic.
✅ Step 1: Define an abstraction
interface PaymentProcessor {
void process(Payment payment);
}
✅ Step 2: Implement different processors
class CreditCardProcessor implements PaymentProcessor {
public void process(Payment payment) {
System.out.println("Processing credit card...");
}
}
class PaypalProcessor implements PaymentProcessor {
public void process(Payment payment) {
System.out.println("Processing PayPal...");
}
}
class StripeProcessor implements PaymentProcessor {
public void process(Payment payment) {
System.out.println("Processing Stripe...");
}
}
✅ Step 3: Refactor CheckoutService
to use the abstraction
class CheckoutService {
private final PaymentProcessor processor;
public CheckoutService(PaymentProcessor processor) {
this.processor = processor;
}
public void pay(Payment payment) {
processor.process(payment);
}
}
Now the system is open for extension — new payment types only require new PaymentProcessor
implementations — and closed for modification — CheckoutService
remains unchanged.
5. Summary
Term | Meaning |
---|---|
Open for extension | You can add new behaviors |
Closed for modification | Existing code does not need to change |
Conclusion
The Open/Closed Principle helps you extend your code without breaking existing functionality. By depending on abstractions, you keep your design flexible, testable, and ready for change.
You can find the complete code of this article here in GitHub.
📚 Related: Liskov Substitution Principle in Java
Pingback: Single Responsibility Principle in Java