Principio de sustitucion de liskov - FernandoCalmet/csharp-essential GitHub Wiki

El principio de sustitución de Liskov (LSP) establece que los objetos de la clase secundaria deberían poder reemplazar los objetos de la clase principal sin comprometer la integridad de la aplicación. Lo que esto significa esencialmente es que debemos esforzarnos por crear tales objetos de clase derivados que puedan reemplazar objetos de la clase base sin modificar su comportamiento. Si no lo hacemos, nuestra aplicación podría terminar estropeándose.

¿Tiene sentido esto para ti?

Para aclarar las cosas, vamos a utilizar un ejemplo simple de "Calculadora de suma", que nos ayudará a comprender cómo implementar mejor el LSP.

Proyecto Inicial

En este ejemplo, vamos a tener una matriz de números y una funcionalidad base para sumar todos los números de esa matriz. Pero digamos que necesitamos sumar solo números pares o impares.

¿Cómo implementaríamos eso? Veamos una forma de hacerlo:

public class SumCalculator
{
    protected readonly int[] _numbers;
    public SumCalculator(int[] numbers)
    {
        _numbers = numbers;
    }
    public int Calculate() => _numbers.Sum();
}
public class EvenNumbersSumCalculator: SumCalculator
{
    public EvenNumbersSumCalculator(int[] numbers)
        :base(numbers)
    {
    }
    public new int Calculate() => _numbers.Where(x => x % 2 == 0).Sum();
}

Ahora, si probamos esta solución, ya sea que calculemos la suma de todos los números o la suma de solo los números pares, seguramente obtendremos el resultado correcto:

class Program
{
    static void Main(string[] args)
    {
        var numbers = new int[] { 5, 7, 9, 8, 1, 6, 4 };
        SumCalculator sum = new SumCalculator(numbers);
        Console.WriteLine($"The sum of all the numbers: {sum.Calculate()}");
        Console.WriteLine();
        EvenNumbersSumCalculator evenSum = new EvenNumbersSumCalculator(numbers);
        Console.WriteLine($"The sum of all the even numbers: {evenSum.Calculate()}");
    }
}

Crear una mejor solución

Como podemos ver, esto está funcionando bien. Pero, ¿qué tiene de malo esta solución entonces?

¿Por qué estamos tratando de arreglarlo?

Bueno, como todos sabemos, si una clase secundaria hereda de una clase principal, entonces la clase secundaria es una clase principal. Teniendo eso en cuenta, deberíamos poder almacenar una referencia en EvenNumbersSumCalculator como una variable SumCalculator y nada debería cambiar. Entonces, echemos un vistazo a eso:

SumCalculator evenSum = new EvenNumbersSumCalculator(numbers);
Console.WriteLine($"The sum of all the even numbers: {evenSum.Calculate()}");

Como podemos ver, no estamos obteniendo el resultado esperado porque nuestra variable evenSum es de un tipo SumCalculator que es una clase de orden superior (una clase base). Esto significa que se ejecutará el método Count de SumCalculator. Entonces, esto no es correcto, obviamente, porque nuestra clase secundaria no se comporta como un sustituto de la clase principal.

Por suerte, la solución es bastante sencilla. Todo lo que tenemos que hacer es implementar pequeñas modificaciones a nuestras dos clases:

public class SumCalculator
{
    protected readonly int[] _numbers;
    public SumCalculator(int[] numbers)
    {
        _numbers = numbers;
    }
    public virtual int Calculate() => _numbers.Sum();
}
public class EvenNumbersSumCalculator: SumCalculator
{
    public EvenNumbersSumCalculator(int[] numbers)
        :base(numbers)
    {
    }
    public override int Calculate() => _numbers.Where(x => x % 2 == 0).Sum();
}

Como resultado, cuando comenzamos nuestra solución, todo funciona como se esperaba y la suma de los números pares vuelve a ser 18.

Entonces, expliquemos este comportamiento. Si tenemos una referencia de objeto secundario almacenada en una variable de objeto principal y llamamos al Calculatemétodo, el compilador utilizará el método Calculate de la clase principal. Pero en este momento, debido a que el método Calculate se define como "virtual" y se anula en la clase secundaria, se usará ese método en la clase secundaria en su lugar.

Implementando el principio de sustitución de Liskov

Aún así, el comportamiento de nuestra clase derivada ha cambiado y no puede reemplazar a la clase base. Entonces necesitamos actualizar esta solución introduciendo la clase abstracta Calculator:

public abstract class Calculator
{
    protected readonly int[] _numbers;
    public Calculator(int[] numbers)
    {
        _numbers = numbers;
    }
    public abstract int Calculate();
}

Entonces tenemos que cambiar nuestras otras clases:

public class SumCalculator : Calculator
{
    public SumCalculator(int[] numbers)
        :base(numbers)
    {
    }
    public override int Calculate() => _numbers.Sum();
}
public class EvenNumbersSumCalculator: Calculator
{
    public EvenNumbersSumCalculator(int[] numbers)
       :base(numbers)
    {
    }
    public override int Calculate() => _numbers.Where(x => x % 2 == 0).Sum();
}

Excelente. Ahora podemos empezar a hacer llamadas hacia estas clases:

class Program
{
    static void Main(string[] args)
    {
        var numbers = new int[] { 5, 7, 9, 8, 1, 6, 4 };
        Calculator sum = new SumCalculator(numbers);
        Console.WriteLine($"The sum of all the numbers: {sum.Calculate()}");
        Console.WriteLine();
        Calculator evenSum = new EvenNumbersSumCalculator(numbers);
        Console.WriteLine($"The sum of all the even numbers: {evenSum.Calculate()}");
    }
}

Volveremos a tener el mismo resultado, 40 para todos los números y 18 para los números pares. Pero ahora, podemos ver que podemos almacenar cualquier referencia de subclase en una variable de clase base y el comportamiento no cambiará, que es el objetivo de LSP.

Lo que ganamos al implementar el LSP

Al implementar el LSP, mantenemos nuestra funcionalidad intacta y nuestras subclases siguen actuando como un sustituto de una clase base.

Además, alentamos la reutilización del código mediante la implementación del LCP y un mejor mantenimiento del proyecto.

Conclusión

Podemos ver que implementar el LSP no es tan complicado sino todo lo contrario. La mayoría de nosotros probablemente ya implementamos este principio muchas veces en nuestro código sin saber su nombre porque en el mundo orientado a objetos, el polimorfismo es algo muy importante.