Open/Closed Principle

The Open/Closed Principle is one of five design principles for object-oriented software development described by Robert C. Martin.

Definition of the Open/Closed Principle

Robert C. Martin considered this principle as the “the most important principle of object-oriented design”. But he wasn’t the first one who defined it. Bertrand Meyer wrote about it in 1988 in his book Object-Oriented Software Construction. He explained the Open/Closed Principle as:

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

The general idea of this principle is great. It tells you to write your code so that you will be able to add new functionality without changing the existing code. That prevents situations in which a change to one of your classes also requires you to adapt all depending classes. Unfortunately, Bertrand Mayer proposes to use inheritance to achieve this goal:

A class is closed, since it may be compiled, stored in a library, baselined, and used by client classes. But it is also open, since any new class may use it as parent, adding new features. When a descendant class is defined, there is no need to change the original or to disturb its clients.

But as we’ve learned over the years and as other authors explained in great details, e.g., Robert C. Martin in his articles about the SOLID principles or Joshua Bloch in his book Effective Java, inheritance introduces tight coupling if the subclasses depend on implementation details of their parent class.

That’s why Robert C. Martin and others redefined the Open/Closed Principle to the Polymorphic Open/Closed Principle. It uses interfaces instead of superclasses to allow different implementations which you can easily substitute without changing the code that uses them. The interfaces are closed for modifications, and you can provide new implementations to extend the functionality of your software.

The main benefit of this approach is that an interface introduces an additional level of abstraction which enables loose coupling. The implementations of an interface are independent of each other and don’t need to share any code. If you consider it beneficial that two implementations of an interface share some code, you can either use inheritance or composition.

Let’s take a look at an example that uses the Open/Closed Principle.

Open/Closed principle for Java

Non-compliant

Let's consider we're building a calculator app that might have several operations, such as addition and subtraction.

First of all, we'll define a top-level interface – CalculatorOperation:

public interface CalculatorOperation {}

Let's define an Addition class, which would add two numbers and implement the CalculatorOperation:

public class Addition implements CalculatorOperation {

    private double left;
    private double right;
    private double result = 0.0;

    public Addition(double left, double right) {
        this.left = left;
        this.right = right;
    }

    // getters and setters
}

As of now, we only have one class Addition, so we need to define another class named Subtraction:

public class Subtraction implements CalculatorOperation {

    private double left;
    private double right;
    private double result = 0.0;

    public Subtraction(double left, double right) {
        this.left = left;
        this.right = right;
    }
    // getters and setters
}

Let's now define our main class, which will perform our calculator operations:

public class Calculator {

    public void calculate(CalculatorOperation operation) {

        if (operation == null) {
            throw new InvalidParameterException("Can not perform operation");
        }

        if (operation instanceof Addition) {
            Addition addition = (Addition) operation;
            addition.setResult(addition.getLeft() + addition.getRight());

        } else if (operation instanceof Subtraction) {
            Subtraction subtraction = (Subtraction) operation;
            subtraction.setResult(subtraction.getLeft() - subtraction.getRight());
        }
    }
}

Although this may seem fine, it's not a good example of the OCP. When a new requirement of adding multiplication or divide functionality comes in, we've no way besides changing the calculate method of the Calculator class.

Hence, we can say this code is not OCP compliant.

OCP Compliant

As we've seen our calculator app is not yet OCP compliant. The code in the calculate method will change with every incoming new operation support request. So, we need to extract this code and put it in an abstraction layer.

One solution is to delegate each operation into their respective class:

public interface CalculatorOperation {
    void perform();
}

As a result, the Addition class could implement the logic of adding two numbers:

public class Addition implements CalculatorOperation {
    private double left;
    private double right;
    private double result;

    // constructor, getters and setters

    @Override
    public void perform() {
        result = left + right;
    }
}

Likewise, an updated Subtraction class would have similar logic. And similarly to Addition and Subtraction, as a new change request, we could implement the division logic:

public class Division implements CalculatorOperation {
    private double left;
    private double right;
    private double result;

    @Override
    public void perform() {
        if (right != 0) {
            result = left / right;
        }
    }
}

And finally, our Calculator class doesn't need to implement new logic as we introduce new operators:

public class Calculator {

    public void calculate(CalculatorOperation operation) {

        if (operation == null) {
            throw new InvalidParameterException("Cannot perform operation");
        }

        operation.perform();
    }
}

That way the class is closed for modification but open for an extension.

References

  • https://stackify.com/solid-design-open-closed-principle/
  • https://www.baeldung.com/java-open-closed-principle

Previous Post Next Post