Do you REALLY know what SOLID principles means? (#2: Exclusive Truth about Open Closed Principle)

Open for Extension, Closed for Modification

The Open Closed Principle is a popular software design principle that states that classes should be open for extension, but closed for modification. This means that we should be able to add new functionality to a class without modifying the existing code for that class. The reason for this is that modifying existing code can introduce potential bugs and risks into our system.

Classes should be open for extension, but closed for modification. By doing so, we stop ourselves from modifying existing code and causing potential new bugs. What Open Close Principle wants to say is – We should be able to add new functionality without touching the existing code for the class. This is because whenever we modify the existing code, we are taking the risk of creating potential bugs.

So we should avoid touching the tested and reliable (mostly) production code if possible. But how are we going to add new functionality without touching the class? It is usually done with the help of interfaces and abstract classes.
We can make sure that our code is compliant with the open/closed principle by utilising inheritance and/or implementing interfaces that enable classes to polymorphically substitute for each other.

Interface and Abstract Class

To add new functionality without modifying a class, we can utilize interfaces and abstract classes. These allow us to add new functionality through inheritance or implementation, without changing the existing code for a class. This allows us to add new features to our software without affecting the existing codebase.

In practice, we need to consider how the requirements of our software are likely to change in the future. We should determine what should be left abstract for our module’s consumers to make concrete, and what concrete functionality we should provide. It’s important to find a balance between being partially concrete (to do something useful) and partially abstract (to be used in a variety of contexts).

Consider the following example:

class Post
{
    void CreatePost(Database db, string postMessage)
    {
        if (postMessage.StartsWith("#"))
        {
            db.AddAsTag(postMessage);
        }
        else
        {
            db.Add(postMessage);
        }
    }
}

This implementation violates the open close principle because it differs the behavior based on the starting letter of the postMessage. If we later wanted to also include mentions starting with ‘@’, we’d have to modify the class with an extra ‘else if’ in the CreatePost() method.

To make this code compliant with the open close principle, we can use inheritance as follows:

class Post
{
    void CreatePost(Database db, string postMessage)
    {
        db.Add(postMessage);
    }
}

class TagPost : Post
{
    override void CreatePost(Database db, string postMessage)
    {
        db.AddAsTag(postMessage);
    }
}

By using inheritance, it is now much easier to create extended behaviour to the Post object by overriding the CreatePost() method. The evaluation of the first character ‘#’ will now be handled elsewhere of our software, and if we want to change the way a postMessage is evaluated, we can change the code there, without affecting any of these underlying pieces of behaviour.

The general idea of this principle is great.

It tells us to write our code so that we will be able to add new functionality without changing the existing code. That prevents situations in which a change to one of our classes also requires us to adapt all depending classes. Unfortunately, many of the tutorial out there proposes (the example given above is one such case; sadly) to use inheritance to achieve this goal.
But more often than not; inheritance introduces tight coupling if the sub-classes depend on implementation details of their parent class.

That’s why we need to redefine the Open/Closed Principle to the Polymorphic Open/Closed Principle. It uses interfaces instead of super-classes to allow different implementations which you can easily substitute without changing the code that uses them. The interfaces are closed for modifications, and we 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.

Open Closed Principle

Tight Coupling

However, it’s important to note that inheritance can sometimes introduce tight coupling between classes, as the subclass may depend on implementation details of the parent class. To avoid this issue, we can utilize the Polymorphic Open Close Principle, which uses interfaces instead of inheritance to enable different implementations to be easily substituted without changing the code that uses them. This approach introduces an additional level of abstraction and allows for loose coupling between implementations, as they are independent of each other and don’t share any code.

An interesting observation is – the word extend doesn’t necessarily mean that we should subclass the actual class that needs the new behaviour. Let’s consider an example:

public class Context {
    private IBehavior behavior;

    public void doStuff() {
        if (this.behavior != null)
            this.behavior.doStuff();
    }

    public void setBehavior(IBehavior behavior) {
        this.behavior = behavior;
    }
}

public interface IBehavior {
    public void doStuff();
}

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

public class HelloWorldBehavior implements IBehavior {
    public void doStuff() {
        System.println("Hello world!");
    }
}

public class GoodByeBehavior implements IBehavior {
    public void doStuff() {
        System.out.println("Good bye cruel world!");
    }
}

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

// in main method
Context c = new Context();

c.setBehavior(new HelloWorldBehavior());
c.doStuff();
// prints out "Hello world!"

c.setBehavior(new GoodByeBehavior());
c.doStuff();
// prints out "Good bye cruel world!"

In the example above the Context is locked for further modifications.
Most programmers would probably want to subclass the class in order to extend it but here we don’t because it assumes it’s behaviour can be changed through anything that implements the IBehavior interface. Using this pattern we can modify the behaviour of the context at runtime, through the setBehavior method as extension point.
So whenever we want to extend the “closed” context class, let’s try do it by sub-classing it’s “open” collaborating dependency.

OCP: How to Follow

just like the SRP, the way we follow this principle in practice is determined by an educated guess about how the requirements on our software are likely to change in future. In the SRP we make a judgement about decomposition and where to draw encapsulation boundaries in our code. In the OCP, we make a judgement about what in our module we will make abstract and leave to our module’s consumers to make concrete, and what concrete functionality to provide by us. Any sensible module must lie in between these extremes: to do something useful it has to be partially concrete, however to be used in a range of contexts it must be partially abstract.

Violating one principle but following the other: Follows OCP but not LSP

Lets say we have the given code:
public interface IPerson {}

public class Boss implements IPerson {
    public void doBossStuff() { ... }
}

public class Peon implements IPerson {
    public void doPeonStuff() { ... }
}

public class Context {
    public Collection<IPerson> getPersons() { ... }
}

This piece of code follows the open-closed principle. If we’re calling the context’s GetPersons method, we’ll get a bunch of persons all with their own implementations. That means that IPerson is closed for modification, but open for extension.
However things take a dark turn when we have to use it:

// in some routine that needs to do stuff with 
// a collection of IPerson:
Collection<IPerson> persons = context.getPersons();
for (IPerson person : persons) {
    // now we have to check the type... :-P
    if (person instanceof Boss) {
        ((Boss) person).doBossStuff();
    }
    else if (person instanceof Peon) {
        ((Peon) person).doPeonStuff();
    }
}

We have to do type checking and type conversion which violates LSP! We can get out of this situation by either doing some pull-up refactoring or implementing a Visitor pattern.
In this case we can simply do a pull up refactoring after adding a general method:

public class Boss implements IPerson {
    // we're adding this general method
    public void doStuff() {
        // that does the call instead
        this.doBossStuff();
    }
    public void doBossStuff() { ... }
}


public interface IPerson {
    // pulled up method from Boss
    public void doStuff();
}

// do the same for Peon

The benefit now is that we don’t need to know the exact type anymore, following LSP:

// in some routine that needs to do stuff with 
// a collection of IPerson:
Collection<IPerson> persons = context.getPersons();
for (IPerson person : persons) {
    // yay, no type checking!
    person.doStuff();
}

Conclusion

Open close principle is about locking the working code down but still keeping it open somehow with some kind of extension points. This is to avoid code duplication by encapsulating the code that changes. It also allows for failing fast as breaking changes are painful (i.e. change one place, break it everywhere else).
For the sake of maintenance the concept of encapsulating change is a good thing, because changes always happen. The Open Close Principle is a valuable design principle that helps us add new functionality to our software without modifying existing code. By utilizing interfaces and abstract classes, we can follow the Polymorphic Open Close Principle and create flexible and decoupled design.

Leave a Comment