Sustitución de Liskov

Por ahora hemos visto los principios de responsabilidad única y de abierto cerrado en este post veremos el tercero de los principios solid el principio de sustitución de Liskov o Liskov substitution principle. 

 

Qué es el principio de substitución de Liskov

El motivo por el que se denomina a este principio “principio de substitución de Liskov” es porque lo describió una señora llamada barbara Liskov en los 70, pero lo hizo de una forma muy matemática y es un paper que está casi todo en álgebra, por ello Robert C. Martin lo redefinió con la siguiente frase “subtipos deben poder ser sustituidos por sus tipos base”.

 

Aún así, sigue siendo una descripción muy abstracta y que no se entiende muy bien. en resumidas cuentas principio de sustitución de Liskov nos habla sobre polimorfismo. En el mundo real el uso de polimorfismo debe utilizarse con cautela ya que es una herramienta poderosa a par de complicada y nos puede hacer acabar en un callejón sin salida.

Una forma muy sencilla de comprender el principio de substitución de Liskov es mostrando un ejemplo de lo opuesto.

imagen liskovComo podemos ver ambos son patos, ambos hacen cuack pero resulta que necesita pilas, por lo que tenemos una abstracción errónea. 

Cuando aplicamos el principio de substitución de Liskov, no hay una forma general de hacerlo y es exclusivo de nuestra aplicación y del funcionamiento de la misma. Con la excepción de la siguiente norma “El sistema no debe de romperse”. 

En resumen: Si tenemos un cliente/servicio que llama a una interfaz A y cambiamos el cliente para llamar a la implementación B de la misma interfaz, el sistema no debe romperse. 

 

Violación del principio de substitución de Liskov

Una forma de ver que estamos violando el principio de substitución de liskov es lanzar una excepción NotImplementedException. Obviamente esta situación no debe de estar en producción jamás.

public void Metodo1(){
    throw new NotSupportedException();
}

La forma más común de violar el principio de substitución de Liskov es si intentamos extraer interfaces. Cuando tenemos una clase, y queremos extraer su interfaz, debemos de tener en cuenta cada uno de los métodos y propiedades que queremos extraer, y no hacerlo con todos ellos. Debemos extraer los únicos que queramos que sean privados. 

 

Ejemplo de principio de substitución Liskov

Para este ejemplo vamos a utilizar el mismo código que vimos en el post anterior sobre el principio abierto cerrado, el cual está disponible para descargar en GitHub.

Para el ejemplo vamos a modificar la clase Almacenamiento llamandola AlmacenamientoFichero y he añadido un método llamado InformacionFichero` para poder ver el ejemplo de la violación del principio mencionado anteriormente. 

public class AlmacenamientoFichero
{
    readonly string path = "C:/temp";
    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)
    {
        return new FileInfo($"{path}/{titulo}.txt");
    }
}

En nuestro ejemplo, queremos cambiar el sistema para que en vez de almacenar en un fichero guarde en una base de datos, por lo que creamos una clase la cual implementa los mismos métodos que Almacenamiento fichero. 

public class AlmacenamientoSQL
{
    public void Guardar(string titulo, string contenido)
    {
    }

    public string Leer(string titulo)
    {
    }

    public FileInfo InformacionFichero(string titulo)
    {
    }
}

Como vemos estamos implementando los mismos métodos que en nuestra clase AlmacenamientoFichero pero ya no trabajamos por ficheros así que lo recomendable seria cambiar los nombres. 

public class AlmacenamientoFichero
{
    readonly string path = "C:/temp";
    public void Guardar(string titulo, string contenido)
    {
        //código
    }

    public string Leer(string titulo)
    {
        //código
    }

    public FileInfo InformacionFichero(string titulo)
    {
        //código
    }
}

public class AlmacenamientoSQL
{
    public void Guardar(string titulo, string contenido)
    {
        //código
    }

    public string Leer(string titulo)
    {
        //código
    }

    public FileInfo InformacionFichero(string titulo)
    {
        throw new NotSupportedException();
    }
}

Para asegurarnos que estamos indicando todo correctamente lo suyo sería que extraigamos una interfaz de la primera de nuestras clases:

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

public class AlmacenamientoSQL : IAlmacenamiento{}
public class AlmacenamientoFichero : IAlmacenamiento{}

Como vemos estamos indicando un método que no va a ser utilizado, debido a que cuando almacenamos en SQL no podemos coger la información del fichero. Esta es la violación indicada anteriormente. ese método no debería estar indicado en la interfaz.

Además vamos a lanzar una excepción en ese método pues nunca podremos ejecutarlo si estamos consultando la base de datos. 

Por supuesto en nuestra clase principal ArticulosServicio debemos cambiar el almacenamiento por la interfaz IAlmacenamiento y he cambiado un poco el código para comprobar que el fichero existe antes de leerlo.

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

    public ArticulosServicio()
    {
        _logging = new DatabaseLog();
        _almacenamiento = new AlmacenamientoFichero();
        _cache = new Cache();
    }

    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 (!_almacenamiento.InformacionFichero(titulo).Exists)
        return null;

        return _almacenamiento.Leer(titulo);

    }
}

Como podemos arreglar el problema de nuestro método LeerInformacionFichero ya que no tiene sentido ninguno. Pero cómo hemos creado una mala implementación de nuestra interfaz no podemos quitar el método.

Como hemos indicado debido a este método, el código se romperá cuando sea ejecutado. Para arreglarlo, podemos cambiar la implementación. Podemos devolver una simulación de un fichero, o indicar un fichero falso, etc. pero esta solución no es apropiada, debido a que puede llevarnos a falsos positivos.  Como vemos esta implementación de la interfaz es muy problemática, porque solo sirve cuando trabajamos con el sistema de ficheros. 

Cómo arreglar el escenario?

Está claro que el objetivo final es quitar el método LeerInformacionFichero de la interfaz IAlmacenamiento

Para ello, debemos cambiar la lógica que nuestra clase AlmacenamientoFichero está ejecutando, y debemos cambiar el método Leer de una forma que nos indique si ese fichero existe o no, antes de leerlo.

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

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

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

Obviamente este ejemplo es muy sencillo, pero en un ejemplo real puede ser mucho más complejo. 

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

 

Conclusion

Mi consejo para evitar este tipo de situaciones es que cuando estés desarrollando  y quieras hacer una clase, pienses en su implementación o en cómo podrías extraerlo en una interfaz en caso de que algún día quieras cambiar.  

Los principios SOLID están altamente relacionados unos con otros y es muy difícil explicar uno al 100% sin mencionar o pasar por alto una mención a uno de los otros