Blog for Junior Developers C#/.NET

Sunday, June 30, 2024

The Open-Closed Principle (OCP) was defined in 1988 by Bertrand Meyer. It states that a software element should be open to extension but closed to modification.

Thanks to the open-closed principle, our systems can be backwards compatible. This rule is especially important if we want to create systems that will have more than one version. If we focus on this principle, when releasing subsequent versions, we will not spoil the previous ones. If we introduce changes that generate a lot of errors in dependent modules, it is a sign that we are probably not following the open-closed principle.

solid-ocp-everything-you-should-know-about-the-open-closed-principle.jpg

SOLID - OCP - Everything You Should Know About the Open-Closed Principle


The class should be closed to modifications of what already exists, but open to extensions, i.e. we have some method that is used in other parts of the system, so we should not modify it anymore because it may break something in other parts of the system. If we have any consumers of our systems, such a change may cause some confusion for them, because if we change methods, there may be a lack of backward compatibility with our methods that we already provide.

Also, this principle is especially important for programmers who create programming libraries that other programmers use. Imagine a situation where you created a class, for example Calculator, that calculates something and packed it in dll'ke. Then you shared it with other developers. These programmers started using the Calculator class and have hundreds of calls to this class in their programs. In the next version, you added a parameter to the Calculator class constructor. Then developers who use your library want to download the latest version, I have to change all their calls to this class. It will certainly be a very time-consuming and not very pleasant job for them.

To follow this rule, you often have to rely on abstraction to keep your systems more flexible.

Similarly to the previously described single responsibility principle, the open-closed principle applies to classes, modules and methods. The OCP principle is related in part to SRP. Typically, if we write SRP-compliant code, we also write OCP-compliant code.

Enough theory, let's move on to examples. Imagine that you are tasked with making a simple logger that can save messages to text files as well as to a database.

Sample code might look like this:


Code #1 not following the open-closed principle:


public enum LogType
{
    File, Database
}

public class Logger
{
    private readonly LogType _logType;

    public Logger(LogType logType)
    {
        _logType = logType;
    }

    public void Log(string message)
    {
        switch (_logType)
        {
            case LogType.File:
                throw new NotImplementedException();
            case LogType.Database:
                throw new NotImplementedException();
            default:
                throw new Exception("Unexpected log type!");
        }
    }
}

An enumeration type has been created - enum, which is passed when creating a new instance of the Logger class. In the Log method, based on the logger type, we determine where the message is actually logged.

For the purposes of this example, we don't need an implementation of what logging in actually looks like. Everything seems fine, we pass the argument and depending on whether we want to save the message in a text file or in the database, this actually happens. However, the Log method does not satisfy the OCP policy because it cannot be closed to new log types. Now, if you wanted to add, for example, logging messages to Excel, you would have to add a new type and change the Log method and add another statement to the switch. This example is quite simple, but what if there were more methods in this class whose operation depended on the logger type - then there would be even more changes.

How can you write this class according to the OCP rule? You have to rely on abstraction.


Code #1 using the open-closed principle:


public interface IMessageLogger
{
    void Log(string message);
}

public class FileLogger : IMessageLogger
{
    public void Log(string message)
    {
        throw new NotImplementedException();
    }
}

public class DatabaseLogger : IMessageLogger
{
    public void Log(string message)
    {
        throw new NotImplementedException();
    }
}

public class Logger
{
    private readonly IMessageLogger _messageLogger;

    public Logger(IMessageLogger messageLogger)
    {
        _messageLogger = messageLogger;
    }

    public void Log(string message)
    {
        _messageLogger.Log(message);
    }
}

As you can see in the example above, all types are classes that implement the IMessageLogger interface. Thanks to this, if we now need to add a new login type, we just need to create a new class that will implement this interface. The Logger class itself does not require any changes. The class meets the OCP assumptions.

Let's move on to the next example. This time we are to create a class for sending SMS, which is to send SMS using the API issued by the smsapi service.


Code #2 not following the open-closed principle:


public class SmsSender
{
    private SmsApiService _smsApiService;
    public SmsSender(SmsApiService smsApiService)
    {
        _smsApiService = smsApiService;
    }

    public void Send(Sms sms)
    {
        _smsApiService.Send(sms);
    }
}

The above SmsSender class has been designed in such a way that it requires providing an instance of the SmsApiService class and based on this argument, the SMS is sent. The code works properly, it was implemented in the dll. Other developers have started using this class, they have a lot of calls in their applications. One day, without going into details, they came to the conclusion to replace the SMS API with SMS Server.

The developer had to make changes to the SmsSender class and changed it to the following.

public class SmsSender
{
    private SerwerSmsService _serwerSmsService;
    public SmsSender(SerwerSmsService serwerSmsService)
    {
        _serwerSmsService= serwerSmsService;
    }

    public void Send(Sms sms)
    {
        _serwerSmsService.Send(sms);
    }
}

Then he released another version of his dll. Unfortunately, after updating, consumers of this dll received compilation errors every time this class was called, because the constructor now accepts an object of the SerwerSmsService class, and not SmsApiService as before. This happened because the open-closed rule was not applied here. What should the initial code of this class look like in this case?


Code #2 using the open-closed principle:


public class SmsSender
{
    private ISmsService _smsService;
    public SmsSender(ISmsService smsService)
    {
        _smsService = smsService;
    }

    public void Send(Sms sms)
    {
        _smsService.Send(sms);
    }
}

Having such a class, if you want to change the SMS sending service provider, just add a class implementing the ISmsService interface and the SmsSender class will not be changed in any way. Consumers of this library do not need to change their calls to this class. After the update, our library is still backwards compatible.

open-closed-principle-example.jpg

SUMMARY


Today I presented you, along with examples, another letter of the SOLID abbreviation, namely O for Open-Closed Principle, or OCP. It says that classes should be closed to modification, but open to extensions. If we make any changes or develop the functionality of the code that works in the production environment, this should be done by extending the code, not by modifying it. We should rely on abstraction, through the use of interfaces or abstract classes and polymorphism. By following the open-closed principle, our system is more stable and if we develop our system, we minimize the need to modify the existing code in many places. It is not always possible to fully adhere to the OCP principle, but it is worth trying.

Author of the article:
Kazimierz Szpin

KAZIMIERZ SZPIN
Software Developer C#/.NET, Freelancer. Specializes in ASP.NET Core, ASP.NET MVC, ASP.NET Web API, Blazor, WPF and Windows Forms.
Author of the blog CodeWithKazik.com

Previous article - SOLID - SRP - Everything You Should Know About the Single Responsibility Principle
Next article - SOLID - LSP - Everything You Should Know About the Liskov Substitution Principle
Comments (1)
Kazimierz Szpin
KAZIMIERZ SZPIN, czwartek, 11 lipca 2024 11:07
What do you think about this article? I'm curious about your opinion.
Dodaj komentarz
© Copyright 2024 CodeWithKazik.com. All rights reserved. Privacy policy.
Design by Code With Kazik and Modest Programmer.