🧑💻 Author: RK ROY
Open/Closed Principle (OCP)
🎯 Definition
"Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification."
- Bertrand Meyer
The Open/Closed Principle states that you should be able to extend the behavior of a system without modifying its existing code. This is achieved through abstraction and polymorphism.
🤔 What Does "Open/Closed" Mean?
- Open for Extension: You can add new functionality and behaviors
- Closed for Modification: Existing code should not be changed when adding new features
Key Concepts
❌ OCP Violation: Modification Required
Let's look at a classic example that violates OCP:
// ❌ VIOLATION: Need to modify existing code for new features
public class DiscountCalculator {
public double calculateDiscount(String customerType, double amount) {
if (customerType.equals("REGULAR")) {
return amount * 0.05; // 5% discount
} else if (customerType.equals("PREMIUM")) {
return amount * 0.10; // 10% discount
} else if (customerType.equals("VIP")) {
return amount * 0.15; // 15% discount
} else if (customerType.equals("GOLD")) { // ❌ NEW: Had to modify existing code
return amount * 0.20; // 20% discount
} else if (customerType.equals("PLATINUM")) { // ❌ NEW: More modifications
return amount * 0.25; // 25% discount
}
return 0;
}
}
Problems with This Design
- Modification Required: Every new customer type requires changing existing code
- Risk of Bugs: Modifying existing code can introduce bugs
- Violation of Single Responsibility: The class has to know about all customer types
- Hard to Test: Need to retest all existing functionality when adding new types
- Tight Coupling: Calculator is tightly coupled to specific customer types
✅ OCP Solution: Extension Without Modification
Let's refactor using abstraction and polymorphism:
1. Create Discount Strategy Interface
// ✅ GOOD: Abstract interface for discount calculation
public interface DiscountStrategy {
double calculateDiscount(double amount);
String getCustomerType();
}
2. Implement Concrete Strategies
// ✅ GOOD: Regular customer discount strategy
public class RegularCustomerDiscount implements DiscountStrategy {
@Override
public double calculateDiscount(double amount) {
return amount * 0.05; // 5% discount
}
@Override
public String getCustomerType() {
return "REGULAR";
}
}
// ✅ GOOD: Premium customer discount strategy
public class PremiumCustomerDiscount implements DiscountStrategy {
@Override
public double calculateDiscount(double amount) {
return amount * 0.10; // 10% discount
}
@Override
public String getCustomerType() {
return "PREMIUM";
}
}
// ✅ GOOD: VIP customer discount strategy
public class VipCustomerDiscount implements DiscountStrategy {
@Override
public double calculateDiscount(double amount) {
return amount * 0.15; // 15% discount
}
@Override
public String getCustomerType() {
return "VIP";
}
}
3. Context Class Using Strategy
// ✅ GOOD: Calculator that works with any discount strategy
public class DiscountCalculator {
private Map<String, DiscountStrategy> strategies;
public DiscountCalculator() {
this.strategies = new HashMap<>();
// Register default strategies
registerStrategy(new RegularCustomerDiscount());
registerStrategy(new PremiumCustomerDiscount());
registerStrategy(new VipCustomerDiscount());
}
public void registerStrategy(DiscountStrategy strategy) {
strategies.put(strategy.getCustomerType(), strategy);
}
public double calculateDiscount(String customerType, double amount) {
DiscountStrategy strategy = strategies.get(customerType);
if (strategy == null) {
throw new IllegalArgumentException("Unknown customer type: " + customerType);
}
return strategy.calculateDiscount(amount);
}
public Set<String> getSupportedCustomerTypes() {
return strategies.keySet();
}
}
🚀 Adding New Features Without Modification
Now we can add new customer types without modifying existing code:
// ✅ NEW: Gold customer discount - NO modification to existing code needed!
public class GoldCustomerDiscount implements DiscountStrategy {
@Override
public double calculateDiscount(double amount) {
return amount * 0.20; // 20% discount
}
@Override
public String getCustomerType() {
return "GOLD";
}
}
// ✅ NEW: Platinum customer with complex logic - still no modification needed!
public class PlatinumCustomerDiscount implements DiscountStrategy {
@Override
public double calculateDiscount(double amount) {
// Complex discount logic
if (amount > 10000) {
return amount * 0.30; // 30% for large purchases
} else if (amount > 5000) {
return amount * 0.25; // 25% for medium purchases
}
return amount * 0.20; // 20% base discount
}
@Override
public String getCustomerType() {
return "PLATINUM";
}
}
// ✅ NEW: Corporate customer with seasonal promotions
public class CorporateCustomerDiscount implements DiscountStrategy {
private boolean isHolidaySeason;
public CorporateCustomerDiscount(boolean isHolidaySeason) {
this.isHolidaySeason = isHolidaySeason;
}
@Override
public double calculateDiscount(double amount) {
double baseDiscount = amount * 0.18; // 18% base
return isHolidaySeason ? baseDiscount * 1.5 : baseDiscount;
}
@Override
public String getCustomerType() {
return "CORPORATE";
}
}
🏗️ Class Diagram After OCP
🎯 Using the OCP Solution
public class ECommerceSystem {
public static void main(String[] args) {
DiscountCalculator calculator = new DiscountCalculator();
// Add new strategies without modifying existing code
calculator.registerStrategy(new GoldCustomerDiscount());
calculator.registerStrategy(new PlatinumCustomerDiscount());
calculator.registerStrategy(new CorporateCustomerDiscount(true)); // Holiday season
// Use the calculator
double amount = 1000.0;
System.out.println("Regular: $" + calculator.calculateDiscount("REGULAR", amount));
System.out.println("Premium: $" + calculator.calculateDiscount("PREMIUM", amount));
System.out.println("VIP: $" + calculator.calculateDiscount("VIP", amount));
System.out.println("Gold: $" + calculator.calculateDiscount("GOLD", amount));
System.out.println("Platinum: $" + calculator.calculateDiscount("PLATINUM", amount));
System.out.println("Corporate: $" + calculator.calculateDiscount("CORPORATE", amount));
System.out.println("\nSupported customer types: " + calculator.getSupportedCustomerTypes());
}
}
🎨 More OCP Examples
Example 1: Shape Area Calculator
❌ Violation
public class AreaCalculator {
public double calculateArea(Object shape) {
if (shape instanceof Circle) {
Circle circle = (Circle) shape;
return Math.PI * circle.getRadius() * circle.getRadius();
} else if (shape instanceof Rectangle) {
Rectangle rectangle = (Rectangle) shape;
return rectangle.getWidth() * rectangle.getHeight();
} else if (shape instanceof Triangle) { // ❌ NEW: Had to modify
Triangle triangle = (Triangle) shape;
return 0.5 * triangle.getBase() * triangle.getHeight();
}
return 0;
}
}
✅ Solution
// Abstract shape interface
public interface Shape {
double calculateArea();
}
// Concrete implementations
public class Circle implements Shape {
private double radius;
public Circle(double radius) { this.radius = radius; }
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
}
public class Rectangle implements Shape {
private double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double calculateArea() {
return width * height;
}
}
// ✅ NEW: Can add new shapes without modifying existing code
public class Triangle implements Shape {
private double base, height;
public Triangle(double base, double height) {
this.base = base;
this.height = height;
}
@Override
public double calculateArea() {
return 0.5 * base * height;
}
}
// Calculator that works with any shape
public class AreaCalculator {
public double calculateTotalArea(List<Shape> shapes) {
return shapes.stream()
.mapToDouble(Shape::calculateArea)
.sum();
}
}
Example 2: Payment Processing
❌ Violation
public class PaymentProcessor {
public void processPayment(String paymentType, double amount) {
if (paymentType.equals("CREDIT_CARD")) {
// Credit card processing logic
System.out.println("Processing credit card payment: $" + amount);
} else if (paymentType.equals("PAYPAL")) {
// PayPal processing logic
System.out.println("Processing PayPal payment: $" + amount);
} else if (paymentType.equals("CRYPTO")) { // ❌ NEW: Modification required
// Cryptocurrency processing logic
System.out.println("Processing crypto payment: $" + amount);
}
}
}
✅ Solution
// Payment strategy interface
public interface PaymentStrategy {
void processPayment(double amount);
String getPaymentType();
boolean isAvailable();
}
// Concrete payment strategies
public class CreditCardPayment implements PaymentStrategy {
private String cardNumber;
private String expiryDate;
public CreditCardPayment(String cardNumber, String expiryDate) {
this.cardNumber = cardNumber;
this.expiryDate = expiryDate;
}
@Override
public void processPayment(double amount) {
// Credit card processing logic
System.out.println("Processing credit card payment: $" + amount);
System.out.println("Card ending in: " + cardNumber.substring(cardNumber.length() - 4));
}
@Override
public String getPaymentType() { return "CREDIT_CARD"; }
@Override
public boolean isAvailable() {
// Check if card is valid and not expired
return true;
}
}
public class PayPalPayment implements PaymentStrategy {
private String email;
public PayPalPayment(String email) { this.email = email; }
@Override
public void processPayment(double amount) {
System.out.println("Processing PayPal payment: $" + amount);
System.out.println("PayPal account: " + email);
}
@Override
public String getPaymentType() { return "PAYPAL"; }
@Override
public boolean isAvailable() { return true; }
}
// ✅ NEW: Can add new payment methods without modifying existing code
public class CryptocurrencyPayment implements PaymentStrategy {
private String walletAddress;
private String coinType;
public CryptocurrencyPayment(String walletAddress, String coinType) {
this.walletAddress = walletAddress;
this.coinType = coinType;
}
@Override
public void processPayment(double amount) {
System.out.println("Processing " + coinType + " payment: $" + amount);
System.out.println("Wallet: " + walletAddress);
}
@Override
public String getPaymentType() { return "CRYPTOCURRENCY"; }
@Override
public boolean isAvailable() {
// Check blockchain network status
return true;
}
}
// Payment processor using strategies
public class PaymentProcessor {
private Map<String, PaymentStrategy> strategies;
public PaymentProcessor() {
this.strategies = new HashMap<>();
}
public void registerPaymentMethod(PaymentStrategy strategy) {
strategies.put(strategy.getPaymentType(), strategy);
}
public void processPayment(String paymentType, double amount) {
PaymentStrategy strategy = strategies.get(paymentType);
if (strategy == null) {
throw new IllegalArgumentException("Unsupported payment type: " + paymentType);
}
if (!strategy.isAvailable()) {
throw new RuntimeException("Payment method not available: " + paymentType);
}
strategy.processPayment(amount);
}
public List<String> getAvailablePaymentMethods() {
return strategies.values().stream()
.filter(PaymentStrategy::isAvailable)
.map(PaymentStrategy::getPaymentType)
.collect(Collectors.toList());
}
}
🛠️ OCP Implementation Patterns
1. Strategy Pattern
// Context class that uses strategies
public class TextFormatter {
private FormattingStrategy strategy;
public void setFormattingStrategy(FormattingStrategy strategy) {
this.strategy = strategy;
}
public String format(String text) {
return strategy.format(text);
}
}
// Strategy interface
public interface FormattingStrategy {
String format(String text);
}
// Concrete strategies - can add more without modifying existing code
public class UpperCaseFormatter implements FormattingStrategy {
public String format(String text) { return text.toUpperCase(); }
}
public class LowerCaseFormatter implements FormattingStrategy {
public String format(String text) { return text.toLowerCase(); }
}
2. Template Method Pattern
// Abstract base class defining the algorithm template
public abstract class DataProcessor {
// Template method - defines the algorithm structure
public final void processData(String data) {
String cleanedData = cleanData(data);
String validatedData = validateData(cleanedData);
String transformedData = transformData(validatedData);
saveData(transformedData);
}
// Steps that can be extended by subclasses
protected abstract String cleanData(String data);
protected abstract String validateData(String data);
protected abstract String transformData(String data);
protected abstract void saveData(String data);
}
// ✅ NEW: Can add new processors without modifying base class
public class CSVDataProcessor extends DataProcessor {
protected String cleanData(String data) { /* CSV-specific cleaning */ return data; }
protected String validateData(String data) { /* CSV validation */ return data; }
protected String transformData(String data) { /* CSV transformation */ return data; }
protected void saveData(String data) { /* Save CSV */ }
}
public class JSONDataProcessor extends DataProcessor {
protected String cleanData(String data) { /* JSON-specific cleaning */ return data; }
protected String validateData(String data) { /* JSON validation */ return data; }
protected String transformData(String data) { /* JSON transformation */ return data; }
protected void saveData(String data) { /* Save JSON */ }
}
3. Plugin Architecture
// Plugin interface
public interface Plugin {
String getName();
String getVersion();
void execute();
boolean isEnabled();
}
// Plugin manager
public class PluginManager {
private List<Plugin> plugins = new ArrayList<>();
public void registerPlugin(Plugin plugin) {
plugins.add(plugin);
System.out.println("Registered plugin: " + plugin.getName());
}
public void executePlugins() {
plugins.stream()
.filter(Plugin::isEnabled)
.forEach(Plugin::execute);
}
public List<Plugin> getPlugins() {
return new ArrayList<>(plugins);
}
}
// ✅ NEW: Can add new plugins without modifying plugin manager
public class BackupPlugin implements Plugin {
public String getName() { return "Backup Plugin"; }
public String getVersion() { return "1.0"; }
public boolean isEnabled() { return true; }
public void execute() { System.out.println("Running backup..."); }
}
public class SecurityScanPlugin implements Plugin {
public String getName() { return "Security Scanner"; }
public String getVersion() { return "2.1"; }
public boolean isEnabled() { return true; }
public void execute() { System.out.println("Running security scan..."); }
}
🔍 How to Identify OCP Violations
Questions to Ask
- Do I need to modify existing classes when adding new features?
- Are there multiple if/else or switch statements based on types?
- Is the class tightly coupled to specific implementations?
- Do I need to recompile and retest existing code for new features?
Warning Signs
- ❌ Long if/else chains or switch statements
- ❌ Frequent modifications to the same classes
- ❌ Hard-coded dependencies
- ❌ Classes that need to "know" about all possible implementations
- ❌ Difficulty adding new features without extensive changes
🎯 Benefits of Following OCP
1. Stability
// ✅ Existing code remains stable when adding new features
public class Logger {
private List<LogHandler> handlers = new ArrayList<>();
public void addHandler(LogHandler handler) {
handlers.add(handler); // No modification to existing code
}
public void log(String message) {
handlers.forEach(handler -> handler.handle(message));
}
}
// ✅ NEW: Add new handlers without touching Logger class
public class SlackLogHandler implements LogHandler {
public void handle(String message) {
// Send to Slack
}
}
2. Easier Testing
@Test
public void testNewDiscountStrategy() {
// Test only the new strategy, existing code unaffected
DiscountStrategy strategy = new StudentDiscountStrategy();
double discount = strategy.calculateDiscount(100.0);
assertEquals(15.0, discount, 0.01);
}
3. Parallel Development
// Team A can work on new payment methods
public class ApplePayPayment implements PaymentStrategy {
// Implementation
}
// Team B can work on new shipping methods
public class DroneShipping implements ShippingStrategy {
// Implementation
}
// Neither team affects the other's work
🚨 Common OCP Mistakes
❌ Over-Engineering
Don't create abstractions prematurely:
// ❌ BAD: Creating unnecessary abstraction
public interface NumberProcessor {
int process(int number);
}
public class AddOneProcessor implements NumberProcessor {
public int process(int number) { return number + 1; }
}
// ✅ BETTER: Simple method until extension is actually needed
public class Calculator {
public int addOne(int number) { return number + 1; }
}
❌ Wrong Abstraction Level
// ❌ BAD: Too specific interface
public interface DatabaseUserRepository {
void saveUserToMySQL(User user);
void saveUserToPostgreSQL(User user);
}
// ✅ BETTER: Proper abstraction level
public interface UserRepository {
void save(User user);
User findById(Long id);
}
🛠️ Refactoring to OCP
Step-by-Step Process
- Identify Variation Points: Find code that changes frequently
- Extract Abstractions: Create interfaces or abstract classes
- Implement Strategies: Create concrete implementations
- Use Dependency Injection: Inject dependencies instead of creating them
- Register/Configure: Use configuration or registration mechanisms
Example Refactoring
// BEFORE: Violation
public class ReportGenerator {
public void generateReport(String type, List<Data> data) {
if (type.equals("PDF")) {
// Generate PDF
} else if (type.equals("Excel")) {
// Generate Excel
}
}
}
// AFTER: Following OCP
public interface ReportFormat {
void generate(List<Data> data, OutputStream output);
}
public class ReportGenerator {
private Map<String, ReportFormat> formats = new HashMap<>();
public void registerFormat(String type, ReportFormat format) {
formats.put(type, format);
}
public void generateReport(String type, List<Data> data, OutputStream output) {
ReportFormat format = formats.get(type);
if (format != null) {
format.generate(data, output);
}
}
}
🎓 Practice Exercise
Exercise: Refactor the Notification System
Here's a system that violates OCP. Can you refactor it?
public class NotificationService {
public void sendNotification(String type, String message, String recipient) {
if (type.equals("EMAIL")) {
// Send email
System.out.println("Email sent to " + recipient + ": " + message);
} else if (type.equals("SMS")) {
// Send SMS
System.out.println("SMS sent to " + recipient + ": " + message);
} else if (type.equals("PUSH")) {
// Send push notification
System.out.println("Push notification sent to " + recipient + ": " + message);
}
}
public boolean isValidRecipient(String type, String recipient) {
if (type.equals("EMAIL")) {
return recipient.contains("@");
} else if (type.equals("SMS")) {
return recipient.matches("\\d{10}");
} else if (type.equals("PUSH")) {
return recipient.length() > 0;
}
return false;
}
}
Solution Approach
- Create
NotificationChannel
interface - Implement
EmailNotification
,SMSNotification
,PushNotification
- Create
NotificationService
that uses strategies - Add registration mechanism for new notification types
📚 Summary
The Open/Closed Principle is about designing software that can grow and evolve without breaking existing functionality. By following OCP:
- ✅ Stability: Existing code remains unchanged and stable
- ✅ Extensibility: New features can be added easily
- ✅ Maintainability: Reduced risk of introducing bugs
- ✅ Testability: New features can be tested in isolation
- ✅ Flexibility: System adapts to changing requirements
Remember: Open for Extension, Closed for Modification!
The key is to identify the parts of your system that are likely to change and abstract them behind stable interfaces. Use design patterns like Strategy, Template Method, and Plugin architectures to achieve OCP compliance.