DEV Community

loading...
Cover image for Open Closed Principle

Open Closed Principle

amrsaeedhosny profile image Amr Saeed Originally published at amrsaeed.com ・3 min read

Now it’s the time to start another journey with the second SOLID principle, the Open-Closed.

Open-Closed principle states:

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

What does that even mean? How could a software entity be opened and closed at the same time!

Let’s start explaining by using a real-world scenario to illustrate the problem, then moving into the solution and the technical details.

class PhotoViewer{
    void openPhoto(){
        System.out.println("Open JPEG photo!");  
    }
}
Enter fullscreen mode Exit fullscreen mode

Now you wrote your photo viewer class, and you’re quite sure that it has to open JPEG photos only. Later you realized that your users are continuously trying to view PNG photos. And because your app doesn’t support, it raises an error. So, you decided to edit the source code so that it supports PNG.

class PhotoViewer{  
    void openPhoto(String type){
        if(type == "JPEG"){      
            System.out.println("Open JPEG photo!");
        }    
        else if(type == "PNG"){
            System.out.println("Open PNG photo!");    
        }
        else{
            System.out.println("Photo type is not supported!");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, what are the problems of your code after making the changes?

What if you decided to add another type? Which is 100% going to happen now or then?

At that moment, you would add another else if statement. Adding a new type after another makes open photo function too large as a function. And because all types exist at the same place, removing some lines of code from one type may crash the others.

So the code is open for extension, which means you can extend it by other features. But not closed for modification, because every time you add a type you modify the open photo function lines of code. Many changes at the same block of code for different reasons is an indication of bad design.

Okay, I know you might have thought about separating JPEG and PNG into different functions at the photo viewer class. I’m with you, let’s try it!

class PhotoViewer{  
    void openJpegPhoto(){    
        System.out.println("Open JPEG photo!");
    }
    void openPngPhoto(){
        System.out.println("Open PNG photo!");
    }
}
Enter fullscreen mode Exit fullscreen mode

Now your code is open for extension. You can add new types easily by adding new functions. But the same problem still exists. The solution is not 100% closed for modification, as the class lines of code are going to be affected by the changes. Also notice that adding another type to the photo viewer class is going to make it larger with more responsibilities. This violates the Single Responsibility Principle.

I know you’re tired, but believe me, it worth it.

The solution is about using Object-Oriented Programming features by using a generic interface to represent a generic photo type that any new type can implement.

interface Photo{
    void open();
}

class PhotoJpeg implements Photo{  
    void open(){    
        System.out.println("Open JPEG photo!");  
    }
}

class PhotoPng implements Photo{
    void open(){    
        System.out.println("Open PNG photo!");  
    }
}

class PhotoViewer(){  
    void openPhoto(Photo photo){    
        photo.open();  
    }
}
Enter fullscreen mode Exit fullscreen mode

You can see that adding a new type won’t change a single line of code from the source code. You only need to implement the photo interface with the new type and send the photo to the photo viewer to be opened.

class PhotoGif implements Photo{  
    void open(){    
        System.out.println("Open GIF photo!");  
    }
}
Enter fullscreen mode Exit fullscreen mode

The solution is 100% open for extension and closed for modification. Notice that the single responsibility principle is also applied.

Now you know that Open-Closed means that your code is open to be extended by new functionalities and closed in terms of changing the source code, but appending to it.

Wait for a new journey with the third SOLID principle, Liskov Substitution!

Photo viewers are used everywhere, literally. You can find them on social media platforms such as facebook or twitter. Also, you can find them on your smartphone as gallery applications. They’re considered a key feature in many apps.

If you decided to develop a photo viewer with your own, users would expect it to open photos of any type. It could be a JPEG photo. It might be a PNG. Sometimes it’s not the case that a specific type is supported by your photo viewer. Hence, you need to change the source code of your photo viewer to support that type.

How to guarantee that a new type added to your photo viewer won’t affect an already existing one? You may edit your application to support a GIF type, but it crashes the application when you open a PNG photo. How to prevent that from happening? And how to guarantee enough flexibility to add and remove other types in the future without affecting each other.

The solution is the Open-Closed principle, as you certainly would guess.

Recalling the PhotoViewer class from the Single Responsibility article with a slight change.

Discussion (8)

pic
Editor guide
Collapse
brucknert profile image
Tomas Bruckner

Does it really solve anything though? Now you have to create a factory that has the same if statement as before:

if(type == "JPEG") {
  return new PhotoJpeg();
}    
else if (type == "PNG"){
  return new PhotoPng();    
}
else {
  return new PhotoGif();
}
Enter fullscreen mode Exit fullscreen mode

When you want to add a new type of image format e.g. webp, you still have to break the 'closed for modification' principle, because you need to change the code of your factory and add new 'if'.

So instead of having one class you now have 1 interface, 3 classes for image types, and 1 factory. When you want to check what types of image formats are available, you have to check all classes that implement the Photo interface. To add a new format, it's also harder because there may be a class already but it is not in a factory yet. Maybe you want to add ".jpg" instead of ".jpeg". Maybe you want to change behavior that for all types even the uppercase format is valid so ".JPG" works as well. How do you share it? You create more classes and add inheritance and now it is even harder to know what's going on. Just layers and layers of abstraction that makes everything really difficult to see.

In the original code, you could tell right away what were all the capabilities of your code and what was going on. The new "better" code is much more complicated, you can't tell what's going on if you don't go through x files and it hasn't achieved anything, because you still need to break the closed for modification principle.

Collapse
amrsaeedhosny profile image
Amr Saeed Author • Edited

I don't know if I got ur point but I'll try to explain further:

This code:

class PhotoViewer{  
    void openPhoto(String type){
        if(type == "JPEG"){      
            System.out.println("Open JPEG photo!");
        }    
        else if(type == "PNG"){
            System.out.println("Open PNG photo!");    
        }
        else{
            System.out.println("Photo type is not supported!");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

is replaced with this one:

class PhotoViewer(){  
    void openPhoto(Photo photo){    
        photo.open();  
    }
}
Enter fullscreen mode Exit fullscreen mode

You don't have to check anything in the latter code. PhotoViewer only cares about dealing with any instance of a Photo class to open it. So, you can have an array of different photos and open any one of them using PhotoViewer. If you try to add a new Photo type you don't have to check anything or change the behavior of the PhotoViewer, just add a new photo. Also, you've to know that if you decided to use the simple Factory pattern to create your photo objects it will obviously violate the Open-Closed principle as you said. Check this link for more details:
softwareengineering.stackexchange....

Violating the Open-Closed principle to create objects using Factory doesn't mean you've to make other parts violate it. This part of the example only explains what the Open-Closed principle says.

Collapse
brucknert profile image
Tomas Bruckner • Edited

I can see that I wasn't clear enough so I will try to explain better.

You have a string representing a photo and PhotoViewer class just as you had before.

var photoType = "jpg";
var photoViewer = new PhotoViewer();
Enter fullscreen mode Exit fullscreen mode

in the first code, you open the photo simply like this

photoViewer.openPhoto(photoType);
Enter fullscreen mode Exit fullscreen mode

but you had "ugly" if inside openPhoto method that you didn't like.

So you refactored it to get rid of it. You created a bunch of classes, an interface. But now you have a problem because you still only have the string representing the photo. How can you change string photoType to correct class implementing Photo interface?

var photoType = "jpg";
var photoViewer = new PhotoViewer();
var photo = ???  // how to solve this?
photoViewer.openPhoto(photo);
Enter fullscreen mode Exit fullscreen mode

you can either create if statement

if (photoType == "jpg") {
  photoViewer(new JpegPhoto());
} else if (photoType == "png") {
 ...
}
Enter fullscreen mode Exit fullscreen mode

so you have the same "if" as before. But now you have a bunch of classes and an interface on top of that. So your code still breaks the principle the same way. There are just more classes + higher abstraction = complex code, but you still haven't solved the core issue why you even started refactoring in the first place and that is that it breaks open-closed principle.

You can also add even more abstraction and make the code even more complex using factory pattern. But again, yes, now the previous code doesn't break the principle:

var photoType = "jpg";
var photoViewer = new PhotoViewer();
var photoFactory = new PhotoFactory()
var photo = photoFactory.create(photoType);
photoViewer.openPhoto(photo);
Enter fullscreen mode Exit fullscreen mode

But once again, it is just shifting the if somewhere else (this time to the factory).

public class PhotoFactory {
  public Photo create(String photoType) {
    if (photoType == "jpg") {
       return new JpegPhoto();
    } else if (photoType == "png") {
       return new PngPhoto();
  }
}
Enter fullscreen mode Exit fullscreen mode

So once again, your code still breaks the principle.

So you had a code that was breaking the principle. You made some changes, added a lot of abstraction and the core problem that your code is breaking the principle is still there.

I just don't understand why shifting and hiding the problem under layers of abstraction is called better code.

Thread Thread
amrsaeedhosny profile image
Amr Saeed Author • Edited

Now I got your point. Actually, you're right about that. The factory pattern somehow violates the open-closed principle, but its violation is not the worst and you can deal with it if you apply the pattern correctly. You can't always apply the principle as the book says. But you can always try to reduce and control the violation.

Imagine if the photo has open, close, and share functions.

class PhotoViewer{  
    void doSomethingToPhoto(String type, String action){
        if(type == "JPEG"){      
            if(action == "open") {
                // do something
            } else if(action == "close") {
                // do something
            } else if(action == "share"){
                // doe something
            }
        }    
        else if(type == "PNG"){
            if(action == "open") {
                // do something
            } else if(action == "close") {
                // do something
            } else if(action == "share"){
                // doe something
            }
        }
        else{
            if(action == "open") {
                // do something
            } else if(action == "close") {
                // do something
            } else if(action == "share"){
                // doe something
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

You can see that this is one of the worst things you can do to add those functionalities. So, the correct way is to create a Photo interface and force every new photo type to implement. Even if that shifted the violation to use the Factory pattern. You can control its behavior and limit the problems to the creation only. Because it's a well-known design pattern.

I hope I answered your question clearly and I will try to edit the example to become clearer. Also, try to read this article it's useful and talks about that:

sergeyzhuk.me/2018/01/25/factory-m...

Thread Thread
leadegroot profile image
Lea de Groot • Edited

Drop the if and generate the class name, quick cut:

If (class_exists($phototype .  ‘Type’)) {
  (new  $phototype)-> open();
} else {
  echo 'Error: '.$phototype.' unsupported';
}
Enter fullscreen mode Exit fullscreen mode

(Untested PHP, but I’m mobile right now)

Collapse
sumit profile image
Sumit Singh • Edited

Loved your explanation mate. Just a suggestion, when you are giving examples of code snippets..just write down the name of the language you are using just after the the tripple backticks (`). It will make code looks much better.

Collapse
amrsaeedhosny profile image
Amr Saeed Author

Thanks for ur feedback I will.

Collapse
gwsounddsg profile image
GW

This is by far, the clearest explanation/example of OCP I have ever read.