Open Closed

 

This is the second post in the SOLID series where we are going to look at the "O," which stands for the Open/Closed Principle (OCP).

 

1 - What is the Open/Closed Principle

This principle states that a class should be open for extension but closed for modification.

What this means is that when you write a class and put it into production, you should not make changes to that class. If you want to change what the class does, it should be open to being extended so you can change its behavior.

However, there’s an exception: you can change a class if you find a bug. In that case, it is allowed to modify the class.

 

1.1 - Practical Case Example

It sounds extreme, but in reality, and especially nowadays with microservices, if you have client applications using that class and you go ahead and change its behavior, it can directly affect all clients that use it. And if no one respects this principle, it could cause, and often does cause, the situation where it affects the entire chain of clients, requiring multiple projects or companies to change their code.

This means that making a simple change in a class can lead to a much larger change, not just in the project using it, but in all projects that reference it in some way.

Note: for this post, I’m going to use the same example as in the previous post.

 

2 - Open Closed Principle Example

This principle is basically about not allowing a class to be modified once it’s in production; instead, you should extend its functionality or modify it in child classes that implement it if you use inheritance, or in another class if you use composition.

 

2.1 - Open Closed Using Inheritance

There are two ways to implement this principle. The first is using inheritance.

One of the ways to achieve this is by marking the parent class methods with the virtual keyword, which, as seen in the post about abstract classes, allows us to override a method completely.

If your language is Java, methods are virtual by default, so you don’t need to add anything.

public class Almacenamiento
{
    string path="C:/temp";
    public virtual void EscribirFichero(string titulo, string contenido)
    {
        File.WriteAllText($"{path}/{titulo}.txt", contenido);
    }

    public virtual string LeerFichero(string titulo)
    {
        return File.ReadAllText($"{path}/{titulo}.txt");
    }
}

By adding the virtual keyword, we have modified this class to be open for extension, and we can do the same with log and cache classes.

If you don’t want a class to be extended, you shouldn’t add the virtual keyword. If it needs to be extended, you can add it in the future if needed.

For example, if we want to change our logging system in a certain critical part of the application so that, in addition to logging the error in a log file, it also saves it to a database:

For this scenario, we should NOT modify the current system because it is already in production. Instead, what we can do is, based on Logging, create a new class:

public class DatabaseLogger : Logging
{
    private LogRepository logRepo { get; set; }
    public DatabaseLogger()
    {
        logRepo = new LogRepository();
    }

    public override void Fatal(string message, Exception e)
    {
        logRepo.AlmacenarError(message, e);
        base.Fatal(message, e);
    }
}

 

The new DatabaseLogger class overrides the functionality of the parent Fatal method, adding saving to the database.

We also see that it continues to call the parent class to maintain the old functionality along with the new one, but if we remove base.Fatal() from the method, it will stop saving to the log file and only save in the database.

We can change it in the main method to see how everything keeps working.

public class ArticulosServicio
{
    private readonly Cache _cache;
    private readonly DatabaseLogger _logging;
    private readonly Almacenamiento _almacenamiento;

    public ArticulosServicio()
    {
        _logging = new DatabaseLogger();
        _almacenamiento = new Almacenamiento();
        _cache = new Cache();
    }

    public void GuardarArticulo(string contenido, string titulo)
    {
        _logging.Info($"vamos a insertar el articulo {titulo}");
        _almacenamiento.EscribirFichero(titulo, contenido);
        _logging.Info($"articulo {titulo} insertado");
        _cache.Add(titulo, contenido);
    }

    public string ConsultarArticulo(string titulo)
    {
        _logging.Info($"Consultar artículo {titulo}");

        string contenido = _cache.Get(titulo);
        if (!string.IsNullOrWhiteSpace(contenido))
        {
            _logging.Info($"Artículo encontrado en la cache {titulo}");
            return contenido;
        }

        _logging.Info($"buscar articulo en el sistema de archivos {titulo}");

        return _almacenamiento.LeerFichero(titulo);
    }
}

 

2.2 - Open Closed Principle Using Composition

The second way, and the most recommended since it usually leads to fewer errors, is using a composition pattern, which allows us to change the functionality at runtime. To really understand how this works, you need to understand dependency injection, which allows us to inject a type instead of instantiating it. For now, in this example, I'll instantiate the classes, but they should be injected.

The example is the same: we are creating a class and we want now, in addition, to save to a database.

Our Logging class goes back to its original state, where it does not use the virtual keyword since we are not going to override the methods now.

public class Logging
{
    public void Info(string message)
    {
        Log.Information(message);
    }
    public void Error(string message, Exception e)
    {
        Log.Error(e, message);
    }
    public void Fatal(string message, Exception e)
    {
        Log.Fatal(e, message);
    }
}

What we need to do is create a new class, which contains a property of our original Logging class type that we initialize in the constructor (it should be injected), and as you can see, we create methods named the same way as the original class, replacing the implementation we want to change.

public class DatabaseLogger  
{
    private readonly LogRepository logRepo;
    private readonly Logging loggingOriginal;
    public DatabaseLogger()
    {
        logRepo = new LogRepository();
        loggingOriginal = new Logging();
    }

    public void Info(string message) => loggingOriginal.Info(message);
    
    public void Error(string message, Exception e) => loggingOriginal.Error(message, e);
    
    public void Fatal(string message, Exception e)
    {
        logRepo.AlmacenarError(message, e);
        loggingOriginal.Fatal(message, e);
    }
}

The best way to make sure these classes include all the methods we need is to force both classes to implement an interface.

public interface ILogger
{
    void Info(string message);
    void Error(string message, Exception e);
    void Fatal(string message, Exception e);
}
public class DatabaseLogger  : ILogger
{
    //rest of the code
}
public class Logging : ILogger
{
    //rest of the code
}

Now, in our main class, we can change the code to inject the ILogger interface and, using dependency injection, decide whether that interface will reference Logging or DatabaseLogger.

public class ArticulosServicio
{
    private readonly Cache _cache;
    private readonly ILogger _logging;
    private readonly Almacenamiento _almacenamiento;

    public ArticulosServicio(ILogger logger)
    {
        _logging = logger;
        _almacenamiento = new Almacenamiento();
        _cache = new Cache();
    }

    public void GuardarArticulo(string contenido, string titulo)
    {
        _logging.Info($"vamos a insertar el articulo {titulo}");
        _almacenamiento.EscribirFichero(titulo, contenido);
        _logging.Info($"articulo {titulo} insertado");
        _cache.Add(titulo, contenido);
    }

    public string ConsultarArticulo(string titulo)
    {
        _logging.Info($"Consultar artículo {titulo}");

        string contenido = _cache.Get(titulo);
        if (!string.IsNullOrWhiteSpace(contenido))
        {
            _logging.Info($"Artículo encontrado en la cache {titulo}");
            return contenido;
        }

        _logging.Info($"buscar articulo en el sistema de archivos {titulo}");

        return _almacenamiento.LeerFichero(titulo);
    }
}

As we can see, if we had implemented the open/closed principle from the start, when extending the functionality, we would not have modified the ArticuloServicio class, thus complying with the single responsibility principle discussed in the previous post.

This post was translated from Spanish. You can see the original one here.
If there is any problem you can add a comment bellow or contact me in the website's contact form

Uso del bloqueador de anuncios adblock

Hola!

Primero de todo bienvenido a la web de NetMentor donde podrás aprender programación en C# y .NET desde un nivel de principiante hasta más avanzado.


Yo entiendo que utilices un bloqueador de anuncios como AdBlock, Ublock o el propio navegador Brave. Pero te tengo que pedir por favor que desactives el bloqueador para esta web.


Intento personalmente no poner mucha publicidad, la justa para pagar el servidor y por supuesto que no sea intrusiva; Si pese a ello piensas que es intrusiva siempre me puedes escribir por privado o por Twitter a @NetMentorTW.


Si ya lo has desactivado, por favor recarga la página.


Un saludo y muchas gracias por tu colaboración

© copyright 2025 NetMentor | Todos los derechos reservados | RSS Feed

Buy me a coffee Invitame a un café