Sobes.tech
Middle+

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:

  1. 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.
  2. 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. Classes CreditCardPayment, PayPalPayment implement it. The OrderProcessor class uses PaymentMethod, 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.

How do you apply the open-closed principle (O) from… - sobes.tech