Abierto Cerrado

Este es el segundo post dentro de la serie de SOLID en el que vamos a ver la “O” que es el patrón abierto cerrado u Open Closed Principle (OCP) en inglés.

 

1 - Qué es el principio de abierto/cerrado 

Este principio nos indica que una clase debe estar abierta para poder extenderla pero cerrada para su modificación. 

Lo que quiere decir que cuando estamos escribiendo una clase y la ponemos en producción no debemos hacer cambios a esa clase. Si queremos cambiar lo que esa clase realiza, debe estar abierta para ser extendida si queremos cambiar su comportamiento. 

Sin embargo tenemos una  excepción y es que SI podemos cambiar una clase si encontramos un bug. en ese caso está permitido modificar la clase.

 

1.1 - Ejemplo caso práctico

Suena muy extremo, pero la realidad, y más ahora con los microservicios, es que si tenemos aplicaciones clientes que utilizan esa clase y nos dedicamos a cambiar su comportamiento puede afectar directamente a todos los clientes que la utilizan. Y si nadie respeta este principio podría, y se da, darse el caso que afecte a toda la cadena de clientes, con lo que múltiples proyectos o empresas deban cambiar su código. 

Lo que implica que hacer un cambio simple en una clase puede llevar a un cambio mucho más gordo ya no solo en el proyecto que estás utilizando sino en todos los que lo referencian en alguna manera. 

nota: para este post voy a utilizar el mismo ejemplo que en el post anterior.

 

2 - Ejemplo principio abierto cerrado

Este principio se basa básicamente en no permitir que una clase se vea modificada una vez está en producción sino que se extienda su funcionalidad o se modifique en las clases hijo que la implementan si utilizamos herencia o se modifique en otra clase si utilziamos composition

 

2.1 - Abierto cerrado utilizando herencia

Tenemos dos formas de implementar este principo, la primera es por herencia.

La primera de las formas de conseguir esto es modificar los métodos de la clase padre con la palabra clave virtual el cual nos permite, como vimos en el post de las clases abstractas, sobrescribir un métod por completo.

Si tu lenguaje es java, los métodos son virtual por defecto, así que no tienes que añadir nada.

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");
    }
}

Al añadir la palabra clave virtual hemos modificado esta clase para ser abierta para extender, y podemos realizar lo mismo con la clase de los logs y de cache. 

En caso de que no quieras extender la clase NO debes añadir la palabra clave virtual. Si ha de ser extendida ya se cambiará si hiciera falta en el futuro.  

Por ejemplo, queremos cambiar nuestro sistema de logs en cierta parte crítica de nuestra apliación, para que además de loguear el error en un fichero de log, lo almacene en una base de datos. 

Para este escenario NO debemos modificar el sistema actual ya que ya esta en produccion. entonces lo que podemos hacer es basándonos en Logging crear una clase nueva :

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);
    }
}

 

La clase nueva DatabaseLogger está sobreescribiendo la funcionalidad que tenemos sobre el método Fatal de la clase padre añadiendo el guardado en la base de datos. 

Además vemos que sigue llamando a la clase padre para mantener la funcionalidad antigua además de la nueva, pero si removemos base.Fatal() del método dejará de guardar en el fichero de log y solo guardaría en la base de datos. 

Podemos cambiar en el método principal para ver cómo todo sigue funcionado.

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 - Principio abierto cerrado utilizando composition

La segunda forma y la más recomendable ya que suele dar lugar a menos errores es utilizar un patrón composition, el cual nos permite cambiar el funcionamiento en tiempo de ejecución. Para entender correctamente el funcionamiento deberemos entender sobre inyección de dependencias, con el que podemos inyectar un tipo en lugar de instanciarlo. Por ahora en este ejemplo, voy a instanciar las clases, pero deberían ser inyectadas. 

El ejemplo es el mismo, estamos creando una clase y queremos que ahora además guarde en una base de datos. 

Nuestra clase de Logging vuelve a su estado original donde no tenía la palabra clave virtual ya que ahora no vamos a sobreescribir los métodos.

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);
    }
}

Lo que debemos hacer es crear una nueva clase, la cual contiene una propiedad del tipo de nuestra clase original Logging que inicializamos en el constructor (deberíamos inyectarla) y como vemos creamos métodos que se llaman igual que los métodos de la clase original, sustituyendo el funcionamiento del que deseamos cambiar.

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);
    }
}

La forma óptima de asegurarnos que estas clases van a contener todos los métodos que necesitamos es obligando a ambas clases a implementar una interfaz.

public interface ILogger
{
    void Info(string message);
    void Error(string message, Exception e);
    void Fatal(string message, Exception e);
}
public class DatabaseLogger  : ILogger
{
    //Resto del código
}
public class Logging : ILogger
{
    //resto del código
}

Y ahora en nuestra clase principal podemos cambiar el código para que inyecte la interfaz ILogger y utilizando inyección de dependencias  decidiremos si esa interfaz será referencia a  Logging o 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);
    }
}

Como podemos observar si hubiéramos implementado el principio abierto/cerrado desde el principio a la hora de extender la funcionalidad, no hubiéramos modificado la clase ArticuloServicio con lo que cumpliriamos con el principio de responsabilidad única visto en el post anterior.