Methods to Implement the Open/Closed Principle (OCP) in Your Code

Anh Trần Tuấn - Sep 4 - - Dev Community

1. Understanding the Open/Closed Principle (OCP)

Image

The Open/Closed Principle is a fundamental concept in object-oriented design that encourages you to build systems that can evolve over time without requiring changes to existing, tested code.

1.1 Definition and Importance

The Open/Closed Principle ensures that a software module can accommodate new functionalities without modifying its existing codebase. This leads to:

  • Reduced Risk of Bugs : New features can be added without altering the stable parts of the code.
  • Increased Flexibility : It becomes easier to extend and adapt the software to new requirements.
  • Improved Maintainability : Changes are localized to specific modules or classes, making the system easier to maintain.

Image

1.2 Real-World Example

Consider a payment processing system. Initially, it might only support credit card payments. As the system evolves, you need to add support for other payment methods like PayPal or Bitcoin.

To effectively implement the Open/Closed Principle, you can use design patterns and techniques that promote extensibility while keeping existing code intact.

The Strategy Pattern is a behavioral design pattern that enables selecting an algorithm's implementation at runtime. This pattern allows for adding new strategies without changing the context in which they are used.

Example Code:

// PaymentStrategy.java
public interface PaymentStrategy {
    void pay(int amount);
}

// CreditCardPayment.java
public class CreditCardPayment implements PaymentStrategy {
    private String cardNumber;

    public CreditCardPayment(String cardNumber) {
        this.cardNumber = cardNumber;
    }

    @Override
    public void pay(int amount) {
        System.out.println("Paid " + amount + " using credit card ending with " + cardNumber);
    }
}

// PaypalPayment.java
public class PaypalPayment implements PaymentStrategy {
    private String email;

    public PaypalPayment(String email) {
        this.email = email;
    }

    @Override
    public void pay(int amount) {
        System.out.println("Paid " + amount + " using PayPal account " + email);
    }
}

// PaymentContext.java
public class PaymentContext {
    private PaymentStrategy strategy;

    public PaymentContext(PaymentStrategy strategy) {
        this.strategy = strategy;
    }

    public void executePayment(int amount) {
        strategy.pay(amount);
    }
}
Enter fullscreen mode Exit fullscreen mode

Demo Code:

public class Main {
    public static void main(String[] args) {
        PaymentStrategy creditCard = new CreditCardPayment("1234-5678-9876-5432");
        PaymentStrategy paypal = new PaypalPayment("user@example.com");

        PaymentContext context = new PaymentContext(creditCard);
        context.executePayment(100);

        context = new PaymentContext(paypal);
        context.executePayment(200);
    }
}
Enter fullscreen mode Exit fullscreen mode

Results:

  • With the Strategy Pattern, adding a new payment method requires creating a new class that implements the PaymentStrategy interface.
  • Existing classes and their functionality remain unchanged, demonstrating OCP effectively.

2.2 Decorator Pattern

The Decorator Pattern is another useful pattern for implementing OCP. It allows for adding new behavior to objects dynamically without altering their structure.

Example Code:

// Coffee.java
public interface Coffee {
    String getDescription();
    double cost();
}

// BasicCoffee.java
public class BasicCoffee implements Coffee {
    @Override
    public String getDescription() {
        return "Basic Coffee";
    }

    @Override
    public double cost() {
        return 5.00;
    }
}

// CoffeeDecorator.java
public abstract class CoffeeDecorator implements Coffee {
    protected Coffee coffee;

    public CoffeeDecorator(Coffee coffee) {
        this.coffee = coffee;
    }
}

// MilkDecorator.java
public class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return coffee.getDescription() + ", Milk";
    }

    @Override
    public double cost() {
        return coffee.cost() + 1.50;
    }
}

// SugarDecorator.java
public class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return coffee.getDescription() + ", Sugar";
    }

    @Override
    public double cost() {
        return coffee.cost() + 0.75;
    }
}
Enter fullscreen mode Exit fullscreen mode

Demo Code:

public class Main {
    public static void main(String[] args) {
        Coffee coffee = new BasicCoffee();
        System.out.println(coffee.getDescription() + " costs $" + coffee.cost());

        coffee = new MilkDecorator(coffee);
        System.out.println(coffee.getDescription() + " costs $" + coffee.cost());

        coffee = new SugarDecorator(coffee);
        System.out.println(coffee.getDescription() + " costs $" + coffee.cost());
    }
}
Enter fullscreen mode Exit fullscreen mode

Results:

  • The Decorator Pattern allows for adding new features (like milk or sugar) without modifying the BasicCoffee class.
  • New decorators can be introduced easily, aligning with OCP principles.

3. Conclusion

Implementing the Open/Closed Principle is essential for creating robust and maintainable software systems. By using design patterns like Strategy and Decorator, you can ensure that your code remains flexible and extensible while safeguarding existing functionality.

If you have any questions or need further clarification, please leave a comment below!

Read posts more at : Methods to Implement the Open/Closed Principle (OCP) in Your Code

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .