How do you apply the open-closed principle (O) from SOLID when designing the architecture of your applications?
sobes.tech AI
Answer from AI
I apply the open-closed principle (O) by using abstractions (interfaces, abstract classes). This allows:
- Adding new functionality without changing existing code. I define an interface that describes behavior. Client code interacts with this interface. To add a new implementation, I create a new class that implements this interface, without modifying classes that use the interface.
- Using polymorphism. Different implementations of the interface or abstract class can be substituted depending on the context, without requiring changes to the code that uses them.
Examples of application:
-
Strategy: I define an interface
PaymentMethod. ClassesCreditCardPayment,PayPalPaymentimplement it. TheOrderProcessorclass usesPaymentMethod, which makes it easy to add new payment methods.// Interface describing the payment method public interface PaymentMethod { void pay(double amount); } // Credit card payment implementation public class CreditCardPayment implements PaymentMethod { @Override public void pay(double amount) { System.out.println("Paying " + amount + " using Credit Card"); // Payment processing logic } } // PayPal payment implementation public class PayPalPayment implements PaymentMethod { @Override public void pay(double amount) { System.out.println("Paying " + amount + " using PayPal"); // Payment processing logic } } // Class that uses the payment method (closed for modification) public class OrderProcessor { private PaymentMethod paymentMethod; public OrderProcessor(PaymentMethod paymentMethod) { this.paymentMethod = paymentMethod; } public void processOrder(double totalAmount) { System.out.println("Processing order..."); paymentMethod.pay(totalAmount); // Using abstraction System.out.println("Order processed."); } } -
Factory Method: I use a factory to create objects. The factory returns objects through a common interface, allowing new object types to be added without changing the code that uses the factory.
// Product interface public interface Product { void display(); } // Concrete product A public class ConcreteProductA implements Product { @Override public void display() { System.out.println("This is Product A"); } } // Concrete product B public class ConcreteProductB implements Product { @Override public void display() { System.out.println("This is Product B"); } } // Abstract factory public abstract class ProductFactory { // Factory method returning a product through the interface public abstract Product createProduct(); } // Concrete factory for product A public class ConcreteFactoryA extends ProductFactory { @Override public Product createProduct() { return new ConcreteProductA(); } } // Concrete factory for product B public class ConcreteFactoryB extends ProductFactory { @Override public Product createProduct() { return new ConcreteProductB(); } } // Client code using the factory (closed for modification) public class Client { private ProductFactory factory; public Client(ProductFactory factory) { this.factory = factory; } public void demonstrateProduct() { Product product = factory.createProduct(); // Using abstraction product.display(); } } -
Decorator: Allows dynamically adding new behavior to objects by wrapping them in decorator objects that implement the same interface.
// Component interface public interface Coffee { double getCost(); String getDescription(); } // Basic component public class SimpleCoffee implements Coffee { @Override public double getCost() { return 5.0; } @Override public String getDescription() { return "Simple Coffee"; } } // Abstract decorator public abstract class CoffeeDecorator implements Coffee { protected Coffee decoratedCoffee; public CoffeeDecorator(Coffee decoratedCoffee) { this.decoratedCoffee = decoratedCoffee; } @Override public double getCost() { return decoratedCoffee.getCost(); } @Override public String getDescription() { return decoratedCoffee.getDescription(); } } // Concrete decorator: Milk public class MilkDecorator extends CoffeeDecorator { public MilkDecorator(Coffee decoratedCoffee) { super(decoratedCoffee); } @Override public double getCost() { return super.getCost() + 1.5; } @Override public String getDescription() { return super.getDescription() + ", Milk"; } } // Concrete decorator: Sugar public class SugarDecorator extends CoffeeDecorator { public SugarDecorator(Coffee decoratedCoffee) { super(decoratedCoffee); } @Override public double getCost() { return super.getCost() + 0.5; } @Override public String getDescription() { return super.getDescription() + ", Sugar"; } } // Client code using components (closed for modification) public class CoffeeShop { public static void main(String[] args) { Coffee myCoffee = new SimpleCoffee(); System.out.println(myCoffee.getDescription() + " costs: $" + myCoffee.getCost()); myCoffee = new MilkDecorator(myCoffee); // Wrapping in decorator System.out.println(myCoffee.getDescription() + " costs: $" + myCoffee.getCost()); myCoffee = new SugarDecorator(myCoffee); // Wrapping in another decorator System.out.println(myCoffee.getDescription() + " costs: $" + myCoffee.getCost()); } }
These approaches make the code more flexible, extendable, maintainable, and resilient to changes.