Segregación de interfaces

En este post vamos a ver el principio de segregación de interfaces dentro de los principios SOLID.

Qué es el principio de segregación de interfaces?

Si has ojeado al post anterior donde hablo del principio de sustitución de Liskov, verías que en cierto punto nombramos las interfaces.

Y sobre ellas es donde nos vamos a centrar en este post, ya que las interfaces son las que nos ayudan a arreglar los problemas que nos puede causar una mala implementación del principio de substitución de Liskov.

Como he indicado anteriormente que los principios solid interactúan entre ellos y por eso es muy difícil verlos exactamente por separado. 

El principio de segregación de interfaces nos indica que “Los clientes no deben ser forzados a depender de métodos que no utilizan

Una forma muy fácil de entender el ejemplo es pensar en quién es el dueño de la interfaz, y para mi, la forma más fácil es ver la interfaz como una dependencia

Por ejemplo nuestra clase cliente utiliza una interfaz, esta interfaz es una dependencia. Lo que quiere decir que si nuestra clase cliente no necesita un método en concreto la interfaz no debe indicarlo. 

Una interfaz debe ser definida por el cliente que consume la interfaz, y no por la clase que la implementa. Debido a este motivo, podemos tener interfaces más pequeñas, lo cual se hace mucho más sencillo de manejar. 

 

Es muy común crear interfaces que tienen un único miembro, así que si eres nuevo en un trabajo, y ves muchas interfaces con un único miembro, no te preocupes, es algo común. 

 

Ejemplo de segregación de interfaces

Para este ejemplo volvemos a utilizar el mismo código que hemos estado utilizando durante esta sección o curso. 

Como habrás podido comprobar la última parte del post anterior es el objetivo que queremos hacer en este básicamente es eliminar el método InformacionFichero y lo que queremos es deshacernos de él porque no lo utilizamos en nuestra clase de AlmacenamientoSQL; Aquí disponemos de la interfaz IAlmacenamiento

public interface IAlmacenamiento
{
    void Guardar(string titulo, string contenido);
    string Leer(string titulo);
    FileInfo InformacionFichero(string titulo);
}

Pero en esta ocasión lo haremos de una forma diferente, como he dicho debemos dejar al nuestro cliente en este caso la clase ArticulosServicio definir la interfaz.

Lo primero que vamos a hacer es mover la responsabilidad de leer la información del fichero que estamos comprobando a un método dentro de nuestra clase ArticuloServicio.

public FileInfo GetFicheroInformation(string titulo)
{
    return _almacenamiento.InformacionFichero(titulo);
}

Además debemos de cambiar el if para que lea de nuestro nuevo método.

if (!GetFicheroInformation(titulo).Exists)
    return null;

Esta acción es para ver cómo podemos fraccionar un poco más nuestra interfaz IAlmacenamiento ya que ahora tenemos un método que no está directamente relacionado, y así  lo podemos utilizar como ejemplo, vamos a extraer esta funcionalidad en una nueva Interfaz, la cual va a tener un único método, el cual va a ser leer la información del fichero.

public interface IFicheroInformacion
{
    FileInfo GetInformacion(string titulo);
}

De esta forma podemos realizar un cambio el cual nos va a añadir esta nueva interfaz a nuestra clase ArticuloServicio y desde el método que acabamos de crear llamamos a nuestra nueva interfaz IFicheroInformacion y no a IAlmacenamiento

public class ArticulosServicio
{
    private readonly Cache _cache;
    private readonly ILogging _logging;
    private readonly IAlmacenamiento _almacenamiento;
    private readonly IFicheroInformacion _ficheroInformacion;

    public ArticulosServicio(IFicheroInformacion ficheroInformacion)
    {
        _logging = new DatabaseLog();
        _almacenamiento = new AlmacenamientoSQL();
        _cache = new Cache();
        _ficheroInformacion = ficheroInformacion;
    }

    public void GuardarArticulo(string contenido, string titulo)
    {
        _logging.Info($"vamos a insertar el articulo {titulo}");
        _almacenamiento.Guardar(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}");

        if (!GetFicheroInformation(titulo).Exists)
            return null;

        return _almacenamiento.Leer(titulo);

    }

    public FileInfo GetFicheroInformation(string titulo)
    {
        return _ficheroInformacion.GetInformacion(titulo);
    }
}

Basándonos en el caso de uso del post anterior, vamos a realizar la misma accion, vamos a utilizar nuestra clase AlmacenamientoSQL en vez de AlmacenamientoFichero por lo que conseguir la información del fichero carece de sentido.

Para ello lo que vamos a hacer es refactorizar la interfaz IAlmacenamiento para quitarle InformacionFichero ya que no es necesario para nuestro caso de uso. 

public interface IAlmacenamiento
{
    void Guardar(string titulo, string contenido);
    string Leer(string titulo);
}

Al SQL tampoco necesitamos el método que acabamos de crear. así que debemos eliminarlo y eliminar también la referencia a la interfaz IFicheroInformacion.

Pero, ahora nos puede fallar en caso de que utilicemos la clase de escribir y guardar en el sistema de archivos (AlmacenamientoFichero). lo que tenemos que hacer es mover esa interfaz como un parámetro en el constructor de nuestra clase AlmacenamientoFichero ya que es la clase cliente de IFicheroInformacion.

public class AlmacenamientoFichero : IAlmacenamiento
{
    readonly string path = "C:/temp";

    private readonly IFicheroInformacion _ficheroInformacion;

    public AlmacenamientoFichero(IFicheroInformacion ficheroInformacion)
    {
        _ficheroInformacion = ficheroInformacion;
    }

    public void Guardar(string titulo, string contenido)
    {
        File.WriteAllText($"{path}/{titulo}.txt", contenido);
    }

    public string Leer(string titulo)
    {
        
        return File.ReadAllText($"{path}/{titulo}.txt");
    }
    
    public FileInfo InformacionFichero(string titulo)
    {
        if (!GetFicheroInformation(titulo).Exists)
            return null;

        return new FileInfo($"{path}/{titulo}.txt");
    }

    private FileInfo GetFicheroInformation(string titulo)
    {
        return _ficheroInformacion.GetInformacion(titulo);
    }
}

Debemos hacer lo mismo con todos los ejemplos. 

CONCLUSIÓN

Una práctica muy común es tener interfaces con un único miembro, bueno a mi esto me parece una locura, porque depende cómo sea el sistema, no es necesario. por ejemplo, en el caso de los log, sería una interfaz para guardar el log como error, otra para guardar el log como fatal, etc.

Esto tiene una ventaja que es muy fácil de extender pero, hay que hacer las cosas con cabeza, si extendemos el caso de uso a un proyecto donde llevamos un par de años trabajando, en caso de hacer una interfaz por método, podemos acabar con millones de interfaces, así que lo mejor, en mi opinión es agruparlas por funcionalidad. siempre y cuando no sean mega-interfaces.