SOLID #3: Liskov substitution Principle con C#

En esta publicación veremos como implementar Liskov substitution Principle usando el lenguaje C#.

Rodríguez Patiño, Eduardo
2020-09-20 | 8,102 lecturas
Actualizado:

Si S es un subtipo de T, entonces los objetos de tipo T en un programa de computadora pueden ser sustituidos por objetos de tipo S (es decir, los objetos de tipo S pueden sustituir objetos de tipo T), sin alterar ninguna de las propiedades deseables de ese programa (la corrección, la tarea que realiza, etc.) - Wikipedia.

En español

En pocas palabras, que una clase hija puede ser usada como si fuera una clase padre sin alterar su comportamiento.

Por ejemplo, cuando trabajamos con herencia creamos una clase hija para que herede las funcionalidades de la clase padre pero lo que pasa es que a veces hay métodos que no queremos usar y al ser heredado quedan accesibles a través de nuestra nueva clase. La solución simple es sobrescribirlos y hacer que arrojen una excepción o dejarlos vacíos pero esto rompe el principio.

El problema

Hagamos un ejemplo simple con animales, donde vamos a crear una clase base que definirá el comportamiento de cada animal y puede ser heredado por otros animales. Pero, habrá un animal que no puedo realizar todo los comportamientos de la clase base.

public class Animal 
{
    public virtual void Run() {

    }

    public virtual void Walk() {

    }

    public virtual void Hunt()
    {

    }
}

public class Tiger : Animal 
{

}

public class Turtle : Animal 
{
    public override void Run() 
    {
        throw new NotImplementedException();
    }

    public override void Hunt()
    {
        throw new NotImplementedException();
    }
}

¿Qué sucede con esto?, no entiendo ..

Pues la tortuga no puede cazar y menos correr pero sus métodos están accesibles, y con esto se obtiene un código que realmente no es útil y producirá un error.

Animal turtle = new Turtle();

// Producirá nuestra excepción
turtle.Hunt();

La solución

Podemos solucionar este caso haciendo uso de interfaces, así creamos un sistema más modular.

public interface IRun 
{
    public void Run();
}

public interface IHunt
{
    public void Hunt();
}

public interface IWalk
{
    public void Walk();
}
public class Tiger : IHunt, IWalk, IRun
{
    public void Hunt()
    {

    }

    public void Run()
    {

    }

    public void Walk()
    {

    }
}
public class Turtle : IWalk
{
    public void Walk() 
    {

    }
}

Miren el uso que podemos darle

Veamos un método que quiere mandar a los animales a cazar.

// Recibe una interfaz de los animales que son cazadores
public void GoToHunt(List<IHunt> hunters) 
{
    // Ahora solo trabaja con todos los que sean cazadores
    foreach (var hunter in hunters) 
    {
        hunter.Hunt();
    }
}

Ya podemos trabajar solo con los animales que puedan cazar, la tortuga queda fuera y no rompe nuestro código.