SOLID Principles #2 – The Open/Closed Principle

The Open/Closed Principle represents the “O” in SOLID. In the words of Bertrand Meyer (who originated the term), it states that “software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification“. What it means is that one should be able to extend the behavior of an entity without modifying its code.

To put it another way, a change in requirements should affect as little existing code as possible.

This principle carries in its core a simple truth, against which it is meant to guard – if a single change to an application requires changing a slew of interdependent modules – diving into their code, adding new “if” statements, modifying hard-coded lists – it means the software in question is very fragile and the change is likely to introduce bugs not only to new code, but also to existing features. Furthermore, code written in such a way is impossible to reuse and harder to test.

Example

How it’s broken

Imagine that you were tasked with building a Spaceship capable of using two different propulsion systems – a Warp Drive and a Holtzman Drive.

public class Spaceship {

    private final PropulsionSystemType type;

    public Spaceship(PropulsionSystemType type) {
        this.type = type;
    }

    public String fly() {
        if (type == PropulsionSystemType.WARP) {
            return "Engaging Warp Drive";
        }
        return "Engaging Holtzman Drive";
    }

    public enum PropulsionSystemType {
        WARP, HOLTZMAN
    }
}

You test-drive your Spaceship design:

public class OpenClosedPrincipleExample {

    public static void main(String[] args) {
        Spaceship spaceshipWarp = new Spaceship(Spaceship.PropulsionSystemType.WARP);
        System.out.println(spaceshipWarp.fly());

        Spaceship spaceshipHoltzman = new Spaceship(Spaceship.PropulsionSystemType.HOLTZMAN);
        System.out.println(spaceshipHoltzman.fly());
    }
}

In return, you get a predictable result:

//Console output:
Engaging Warp Drive
Engaging Holtzman Drive

All seems well, but at the last minute you are ordered to add a new propulsion system, the RF Resonant Cavity Thruster, also known as an EmDrive, and so you add a new engine type:

public enum PropulsionSystemType {
    WARP, HOLTZMAN, EM_DRIVE
}

You decide to test the new system:

public class OpenClosedPrincipleExample {

    public static void main(String[] args) {
        Spaceship spaceshipWarp = new Spaceship(Spaceship.PropulsionSystemType.WARP);
        System.out.println(spaceshipWarp.fly());

        Spaceship spaceshipHoltzman = new Spaceship(Spaceship.PropulsionSystemType.HOLTZMAN);
        System.out.println(spaceshipHoltzman.fly());

        Spaceship spaceshipEm = new Spaceship(Spaceship.PropulsionSystemType.EM_DRIVE);
        System.out.println(spaceshipEm.fly());
    }
}

Unfortunately, in your haste, you forgot to modify the Spaceship class to actually support the new propulsion system. And really, who could blame you? In a real-world scenario, the usage could span many classes, each potentially being hundreds of lines long – you can’t be expected to check every existing piece of code in the application each time you make a change. Regardless, the result is incorrect:

//Console output:
Engaging Warp Drive
Engaging Holtzman Drive
Engaging Holtzman Drive

How to fix it

The antidote to that dilemma is to write the Spaceship entity in such a way that as much of the existing code can be reused without modifying it and the Spaceship’s behavior can be extended by creating new classes. In this case, we can use the Strategy Pattern. Let’s start by defining a PropulsionSystem interface:

public interface PropulsionSystem {
    String engage();
}

Now we can define our propulsion systems:

public class WarpDrive implements PropulsionSystem {
    
    @Override
    public String engage() {
        return "Engaging Warp Drive";
    }
}

public class HoltzmannDrive implements PropulsionSystem {

    @Override
    public String engage() {
        return "Engaging Holtzman Drive";
    }
}

public class EmDrive implements PropulsionSystem {

    @Override
    public String engage() {
        return "Engaging EmDrive";
    }
}

Now that we have those, let’s change our Spaceship code to make it closed for modification but open for extension (at least in the case of adding new propulsion systems):

public class Spaceship {

    private final PropulsionSystem propulsionSystem;

    public Spaceship(PropulsionSystem propulsionSystem) {
        this.propulsionSystem = propulsionSystem;
    }

    public String fly() {
        return propulsionSystem.engage();
    }
}

Let’s take our new Spaceship design for a spin:

public class OpenClosedPrincipleExample {

    public static void main(String[] args) {
        Spaceship spaceshipWarp = new Spaceship(new WarpDrive());
        System.out.println(spaceshipWarp.fly());
        
        Spaceship spaceshipHoltzman = new Spaceship(new HoltzmanDrive());
        System.out.println(spaceshipHoltzman.fly());
        
        Spaceship spaceshipEm = new Spaceship(new EmDrive());
        System.out.println(spaceshipEm.fly());
    }
}

Unsuprisingly, everything works.

//Console output:
Engaging Warp Drive
Engaging Holtzman Drive
Engaging EmDrive

What happens if we now want to add another propulsion system type? We don’t have to modify the Spaceship ever again, only needing to write a new class implementing the PropulsionSystem interface:

public class Hyperdrive implements PropulsionSystem {

    @Override
    public String engage() {
        return "Engaging Hyperdrive";
    }
}
public class OpenClosedPrincipleExample {

    public static void main(String[] args) {
        Spaceship spaceshipWarp = new Spaceship(new WarpDrive());
        System.out.println(spaceshipWarp.fly());
        
        Spaceship spaceshipHoltzman = new Spaceship(new HoltzmanDrive());
        System.out.println(spaceshipHoltzman.fly());
        
        Spaceship spaceshipEm = new Spaceship(new EmDrive());
        System.out.println(spaceshipEm.fly());
        
        Spaceship spaceshipHyper = new Spaceship(new Hyperdrive());
        System.out.println(spaceshipHyper.fly());
    }
}
//Console output:
Engaging Warp Drive
Engaging Holtzman Drive
Engaging EmDrive
Engaging Hyperdrive

This code is demonstrably much easier to maintain, more flexible and less prone to breaking. Not only did we not have to add another “if” statement to the Spaceship class, we also did not have to add a new type to the PropulsionSystemType enum (which no longer exists within the code).

We can now outsource building of new propulsion systems and the people working on them don’t need to know how our Spaceship works as long as they use our PropulsionSystem interface. In addition, we can be fairly sure that introducing a new propulsion system won’t break existing ones.

Conclusion

Whenever you need to alter existing code to add new functionality, you run the risk of introducing new bugs to existing features. Therefore, there should be no doubt that following the Open/Closed Principle is incredibly important when writing any advanced application and we should always do our best to create entities which can be extended instead of having to be modified – such practice will lead to code that is both reusable and maintainable, as well as make delivering new features faster (at the small expense of some forethought). Additionally, you must remember that this principle applies to more than just classesevery piece of your system will benefit from being designed in this way and that includes things as small as methods, as well as those as big as modules or whole microservices. Extending software through plugins can be considered a good example of this principle.

You must, however, bear in mind, that no code will ever be completely “closed for modification”, as we simply cannot imagine all possible features it will have to support in its lifetime. As an example, if we wanted our Spaceship to be able to disengage its propulsion system, we would now have to modify the Spaceship class. It is, therefore, important to consciously design our entities, so that they are closed against the most likely changes. Recognizing which changes to guard against takes practice and often requires some amount of domain knowledge. It is prudent to act if you find yourself often coming back to a certain entity – a previously unrecognized abstraction might be lurking there which would allow you to close that entity, making your system more resilient.

Photo by Chuttersnap

Daniel Frąk Written by:

One Comment

Leave a Reply

Your email address will not be published. Required fields are marked *