Responsabilidad única

En este post veremos que es el principio de responsabilidad única o Single Responsibility Principle (SRP) de sus siglas en inglés. 

 

1 - Qué es el principio de responsabilidad única?

La descripción más famosa de este principio es la del propio autor del mismo Robert C. Martin, que dijo “Una clase debe tener solo una razón para cambiar”. 

Bien pero esto, qué quiere decir.

Una parte crucial cuando escribimos código es que nuestro código sea fácil de mantener y de leer. La forma de cumplir con esta premisa es que cada clase debe hacer una cosa y hacerla bien

Cuando tenemos clases que realizan más de una tarea acaban acoplándose unas con otras cuando no deberían estar juntos haciendo esa clase mucho más difícil de usar y entender, comprender y por supuesto mantener.

Una ventaja de mantener código con las clases muy diferenciadas es que son más fáciles de testear, lo que implica que es mucho más difícil tener un bug. 

Un ejemplo rápido de que el principio de responsabilidad única funciona es unix ya que unix originalmente era un sistema de línea de comandos donde cada comando es un pequeño script y este hace lo que se le pide correctamente. 

 

2 - Ejemplo de principio de responsabiliad única

Para entender el concepto de principio de responsabilidad única lo más fácil es que veamos un ejemplo.

Este ejemplo está ultra reducido para entenderlo correctamente. 

Disponemos de una clase en nuestro código en nuestro caso que tiene un método el cual nos permite insertar un artículo y otro método que permite leer ese artículo.

public void GuardarArticulo(string contenido, string titulo)
{
    Log.Information($"vamos a insertar el articulo {titulo}");
    File.WriteAllText($"{path}/{titulo}.txt", contenido);
    Log.Information($"articulo {titulo} insertado");
    this.Cache.Add(titulo, contenido);
}

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

    string contenido = this.Cache.Get(titulo);
    if (!string.IsNullOrWhiteSpace(contenido))
    {
        Log.Information($"Artículo encontrado en la cache {titulo}");
        return contenido;
    }

    Log.Information($"buscar articulo en el sistema de archivos {titulo}");
    contenido = File.ReadAllText($"{path}/{titulo}.txt");

    return contenido;
}

En estos dos métodos debemos Identificar qué factores o que código nos llevarían a tener que modificar la clase y podemos encontrar los siguientes: 

  • Realizamos múltiples logs.
  • Almacenamos el artículo, en este caso en el sistema de archivos.
  • El sistema de cache. 

Como vemos disponemos de 3 puntos.

  • Por ejemplo podríamos cambiar los logs, de serolog a log4net o a otro personalizado por nosotros.
  • Podríamos querer almacenar los artículos en una base de datos en vez de en el sistema de ficheros.
  • Si queremos cambiar el sistema de cache. 

Por estos motivos el principio de responsabilidad única no se estaría cumpliendo, ya que si cambiamos el sistema de log, deberemos cambiar una clase que lee artículos, lo cual no es correcto. 

Pero no solo eso, en este ejemplo disponemos de un cuarto motivo, que es la lógica de los propios métodos. osea, como las tres funcionalidades actúan entre sí para devolvernos la información que estamos buscando. 

 

2.1 - Arreglar código para cumplir el principio de responsabilidad única.

Para cumplir con el principio de responsabilidad única lo que debemos hacer es coger cada uno de esos motivos que nos pueden hacer cambiar la clase y extraerlos en una clase nueva.

Por ejemplo el sistema de log. Debemos separarlo en una clase nueva.

Y esta clase  es la que va a encargarse de que nuestros logs se inserten correctamente ya sea utilizando serilog o log4net

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

Posteriormente deberemos indicar en la clase que estabamos que utilize esa clase para hacer logs y cambiar las variables. 

public class ArticulosServicio
{

    private readonly Logging _logging;

    public ArticulosServicio()
    {
        _logging = new Logging();
    }

    public void GuardarArticulo(string contenido, string titulo)
    {
        _logging.Info($"vamos a insertar el articulo {titulo}");
        File.WriteAllText($"{path}/{titulo}.txt", contenido);
        _logging.Info($"articulo {titulo} insertado");
        this.Cache.Add(titulo, contenido);
    }
    //Resto del código
}

2.2 - En qué mejora nuestro código

Ahora la cuestión es entender en qué mejora realizar esta acción nuestro código.

En el ejemplo estoy utilizando serilog el cual cuando queremos loguear una excepción lo haríamos de la manera

Log.Error(exception, message);

Mientras que si cambiamos a log4net (otra librería para logs) se realiza con un pequeño cambio.

Log.Error(message, exception);

 

serilog vs logfornet

 

Este cambio implica que si queremos cambiar la forma en la que la aplicación registra los logs deberemos hacerlo una única vez en la clase Logging y no en cada una donde estamos llamando a los logs. 

Debemos hacer exactamente lo mismo para cada uno de los motivos o factores que teníamos para cambiar nuestro código. con lo que tendremos que crear una clase para hacer logs, una clase para hacer controlar la caché y una clase para acceder/crear a los ficheros. 

public class Cache
{
    private Dictionary<string, string> CacheDicctionary;
    public Cache()
    {
        CacheDicctionary = new Dictionary<string, string>();
    }

    public void Add(string titulo, string contenido)
    {
        if(!CacheDicctionary.TryAdd(titulo, contenido))
        {
            CacheDicctionary[titulo] = contenido;
        }
    }

    public string Get(string titulo)
    {
        CacheDicctionary.TryGetValue(titulo, out string contenido);
        return contenido;
    }

}

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

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

Como puedes imaginar, si una clase es muy grande puede contener muchas clases adicionales a las que hace referencia, para ello SOLID incluye dependency injection que veremos más adelante en esta serie de posts, este es el resutlado final:

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

    public ArticulosServicio()
    {
        _logging = new Logging();
        _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);
    }
}

 

Conclusión

  • Cuando aplicamos el principio de responsabilidad única estamos haciendo que nuestro código sea más fácil de entender, ya que solo realiza una única funcionalidad.
  • El código será más fácil de mantener, ya que los cambios será únicamente en las clases que se ven afectadas directamente y reduciremos el riesgo de romper código que no está afectado.
  • Nuestro código es más reusable ya que cada clase tiene una responsabilidad, si queremos acceder a cierta funcionalidad deberemos pasar por esa clase.