How do you apply the open-closed principle from SOLID in your designs?
sobes.tech AI
Answer from AI
I apply the Open/Closed Principle (OCP), focusing on extendability without modifying existing code.
Key approaches:
-
Using abstractions (interfaces and abstract classes): My code depends on abstractions rather than concrete implementations. This allows adding new implementations without changing existing code that uses these abstractions.
// Interface representing a contract public interface PaymentProcessor { void processPayment(BigDecimal amount); } // Concrete implementation public class CreditCardPaymentProcessor implements PaymentProcessor { @Override public void processPayment(BigDecimal amount) { // Credit card processing logic System.out.println("Processing credit card payment: " + amount); } } // Another implementation public class PayPalPaymentProcessor implements PaymentProcessor { @Override public void processPayment(BigDecimal amount) { // PayPal processing logic System.out.println("Processing PayPal payment: " + amount); } } // Class using abstraction, closed for modification but open for extension with new PaymentProcessors public class Order { private PaymentProcessor paymentProcessor; public Order(PaymentProcessor paymentProcessor) { this.paymentProcessor = paymentProcessor; } public void finalizeOrder(BigDecimal totalAmount) { paymentProcessor.processPayment(totalAmount); // Additional order logic } } -
Strategy Pattern: This design pattern is a direct embodiment of OCP. It allows defining a family of algorithms, encapsulating each one, and making them interchangeable.
// Strategy interface interface ShippingStrategy { BigDecimal calculateCost(BigDecimal weight); } // Concrete strategy class StandardShipping implements ShippingStrategy { @Override public BigDecimal calculateCost(BigDecimal weight) { return weight.multiply(new BigDecimal("2.5")); } } // Another strategy class ExpressShipping implements ShippingStrategy { @Override public BigDecimal calculateCost(BigDecimal weight) { return weight.multiply(new BigDecimal("5.0")).add(new BigDecimal("10.0")); } } // Context using strategy, closed for modification but open for new strategies class Order { private ShippingStrategy shippingStrategy; public Order(ShippingStrategy shippingStrategy) { this.shippingStrategy = shippingStrategy; } public BigDecimal getShippingCost(BigDecimal weight) { return shippingStrategy.calculateCost(weight); } } -
Template Method Pattern: Defines the skeleton of an algorithm in a superclass, leaving details to subclasses. This allows changing steps of the algorithm without altering its structure.
// Abstract class with template method abstract class DataProcessor { // Template method public final void processData() { readData(); transformData(); writeData(); } // Abstract methods to be overridden in subclasses abstract void readData(); abstract void transformData(); abstract void writeData(); } // Concrete processor extending the template class CSVDataProcessor extends DataProcessor { @Override void readData() { System.out.println("Reading data from CSV..."); } @Override void transformData() { System.out.println("Transforming CSV data..."); } @Override void writeData() { System.out.println("Writing data to database..."); } } -
Factory Methods: Allow creating objects by providing an interface for creation but leaving the choice of concrete class to subclasses. This adds flexibility in object creation without changing the client code that uses the factory.
interface Product { void use(); } class ConcreteProductA implements Product { @Override public void use() { System.out.println("Using Product A"); } } class ConcreteProductB implements Product { @Override public void use() { System.out.println("Using Product B"); } } abstract class Creator { public void operation() { Product product = factoryMethod(); product.use(); } // Factory method protected abstract Product factoryMethod(); } class ConcreteCreatorA extends Creator { @Override protected Product factoryMethod() { return new ConcreteProductA(); } } class ConcreteCreatorB extends Creator { @Override protected Product factoryMethod() { return new ConcreteProductB(); } }
By applying these approaches, I create code that is easier to maintain, extend, and test, minimizing the risk of errors when adding new functionality.