Saltar al contenido
Simplificación del principio de sustitución de Liskov de SOLID en C#

Simplificación del principio de sustitución de Liskov de SOLID en C#

El Principio de Sustitución de Liskov dice que el objeto de una clase derivada debe ser capaz de reemplazar a un objeto de la clase base sin traer ningún error en el sistema o modificar el comportamiento de la clase base.

6 minutos de lectura

Antes de comenzar a escribir este artículo, quiero agradecer a Steve Smith por su excelente curso sobre el mismo tema con Pluralsight. Este post está inspirado en ese curso.

El Principio de Sustitución de Liskov dice que el objeto de una clase derivada debe ser capaz de reemplazar a un objeto de la clase base sin traer ningún error en el sistema o modificar el comportamiento de la clase base.

En resumen: si S es un subconjunto de T, un objeto de T podría ser reemplazado por un objeto de S sin afectar al programa y traer ningún error en el sistema. Supongamos que tienes una clase Rectangle y otra clase Square. Square es como Rectangle, o en otras palabras, hereda la clase Rectangle. Por lo tanto, como dice el principio de sustitución de Liskov, deberíamos poder reemplazar el objeto de rectángulo por el objeto de cuadrado sin que ocurra ningún cambio o error indeseable en el sistema.

Echemos un vistazo más de cerca a este principio con algunos ejemplos.

Understanding the problem

Digamos que tenemos dos clases, Rectángulo y Cuadrado. En este ejemplo, la clase Square hereda la clase Rectangle. Ambas clases se crean como se indica a continuación:

public class Rectangle
  {
      public virtual int Height { get; set; }
      public virtual int Width { get; set; }
  }

La clase Square hereda la clase Rectangle e invalida las propiedades, como se muestra en la lista siguiente:

public class Square : Rectangle
  {
      private int _height;
      private int _width;
      public override int Height
      {
          get
          {
              return _height;
          }
          set
          {
              _height = value;
              _width = value;
          }
      }
      public override int Width
      {
          get
          {
              return _width;
          }
          set
          {
             _width = value;
             _height = value;
          }
      }
 
  }

Necesitamos calcular el área del Rectángulo y el Cuadrado.  Para este propósito, vamos a crear otra clase llamada AreaCalculator.

public class AreaCalculator
{
     public static int CalculateArea(Rectangle r)
     {
         return r.Height * r.Width;
     }
 
     public static int CalculateArea(Square s)
     {
         return s.Height * s.Height;
     }
}

Sigamos adelante y escribamos pruebas unitarias para calcular el área del rectángulo y el cuadrado. Una prueba unitaria para calcular estas áreas, como se muestra en el listado a continuación, debería pasar.

[TestMethod]
public void Sixfor2x3Rectangle()
{
      var myRectangle = new Rectangle { Height = 2, Width = 3 };
      var result = AreaCalculator.CalculateArea(myRectangle);
      Assert.AreEqual(6, result);
}

Por otro lado, también se debe pasar una prueba para calcular el área del Cuadrado:

[TestMethod]
public void Ninefor3x3Squre()
{
    var mySquare = new Square { Height = 3 };
    var result = AreaCalculator.CalculateArea(mySquare);
    Assert.AreEqual(9, result);
}

En ambas pruebas, estamos creando:

1. El objeto del rectángulo para encontrar el área del rectángulo

2. El objeto del cuadrado para encontrar el área del cuadrado

Y las pruebas pasan como se esperaba. Ahora sigamos adelante y creemos una prueba en la que intentaremos sustituir el objeto de Rectángulo con el objeto de Cuadrado. Queremos encontrar el área de Rectángulo usando el objeto de Cuadrado y para la prueba unitaria para esto se escribe a continuación:

[TestMethod]
public void TwentyFourfor4x6RectanglefromSquare()
{
     Rectangle newRectangle = new Square();
     newRectangle.Height = 4;
     newRectangle.Width = 6;
     var result = AreaCalculator.CalculateArea(newRectangle);
     Assert.AreEqual(24, result);
}

La prueba anterior fallaría porque el resultado esperado es 24, sin embargo, el área real calculada sería 36.

Este es el problema. Aunque la clase Square es un subconjunto de la clase Rectangle, la clase Object of Rectangle no puede sustituirse por el objeto de la clase Square sin causar un problema en el sistema. Si el sistema se adhirió al Principio de Sustitución de Liskov, puede evitar el problema anterior.

Resolver un problema con No-Inheritance

Podemos resolver el problema anterior siguiendo los pasos a continuación:

  1. Deshazte de la clase AreaCalculator.
  2. Let each shape define its own Area method.
  3. En lugar de que la clase Square herede la clase Rectangle, vamos a crear una clase base abstracta común Shape y ambas clases heredarán eso.

Se puede crear una forma de clase base común como se muestra en la lista a continuación:

public abstract class Shape
{
 
}

Next, the Rectangle class can be rewritten as follows:

public class Rectangle :Shape
{
    public  int Height { get; set; }
    public  int Width { get; set; }
    public int Area()
    {
       return Height * Width;
    }
}

Y la clase Square se puede reescribir como se muestra en la lista a continuación:

public class Square : Shape
  {
      public int Sides;
      public int Area()
      {
          return Sides * Sides;
      }
 
  }

Ahora podemos escribir una prueba unitaria para la función de área en la clase Rectangle como se muestra en la lista a continuación:

[TestMethod]
public void Sixfor2x3Rectangle()
{
     var myRectangle = new Rectangle { Height = 2, Width = 3 };
     var result = myRectangle.Area();
     Assert.AreEqual(6, result);
}

La prueba anterior debería pasar sin ninguna dificultad. De la misma manera, podemos probar unitariamente la función Area de la clase Square como se muestra en el siguiente listado:

[TestMethod]
public void Ninefor3x3Squre()
{
     var mySquare = new Square { Sides = 3 };
     var result = mySquare.Area();
     Assert.AreEqual(9, result);
}

A continuación, sigamos adelante y escribamos la prueba en la que sustituiremos el objeto de Forma por los objetos de Rectángulo y Cuadrado.

public void TwentyFourfor4x6Rectangleand9for3x3Square()
{
	var shapes = new List<Shape>{
		new Rectangle{Height=4,Width=6},
		new Square{Sides=3}
	};
	var areas = new List<int>();
	foreach(Shape shape in shapes){
		if(shape.GetType()==typeof(Rectangle))
		{
			areas.Add(((Rectangle)shape).Area());
		}
		if (shape.GetType() == typeof(Square))
		{
			areas.Add(((Square)shape).Area());
		}

	}
	Assert.AreEqual(24, areas[0]);
	Assert.AreEqual(9, areas[1]);
}

La prueba anterior pasará y podremos sustituir con éxito los objetos sin afectar el sistema. Sin embargo, hay un problema con el enfoque anterior: estamos violando el principio de abierto-cerrado. Cada vez que una nueva clase hereda la clase Shape, tendremos que agregar una más si la condición está en la prueba y ciertamente no queremos esto.

El problema anterior se puede resolver modificando la clase Shape como se muestra en la lista a continuación:

public  abstract class Shape
{
    public abstract int Area();
}

Aquí hemos movido un método abstracto Area en la clase Shape y cada subclase dará su propia definición al método Area. Las clases Rectangle y Square se pueden modificar como se muestra en el listado a continuación:

public class Rectangle :Shape
{
    public  int Height { get; set; }
    public  int Width { get; set; }
    public override int Area()
    {
        return Height * Width;
    }
}
public class Square : Shape
{
   public int Sides;
   public override int Area()
   {
       return Sides * Sides;
   } 
}

Aquí, las clases anteriores siguen el principio de sustitución de Liskov, y podemos reescribir la prueba sin condiciones "si" como se muestra en la lista a continuación:

[TestMethod]
public void TwentyFourfor4x6Rectangleand9for3x3Square()
{
	var shapes = new List<Shape>{
		new Rectangle{Height=4,Width=6},
		new Square{Sides=3}
	};
	var areas = new List<int>();
 
	foreach (Shape shape in shapes)
	{
		areas.Add(shape.Area());
	}
	Assert.AreEqual(24, areas[0]);
	Assert.AreEqual(9, areas[1]);
}

De esta manera, podemos crear una relación entre la subclase y la clase base adhiriéndonos al Principio de Sustitución de Liskov. Las formas comunes de identificar violaciones de los principios de LS son las siguientes:

  1. Un método no implementado en la subclase.
  2. La función de subclase anula el método de la clase base para darle un nuevo significado.

Espero que encuentres útil este post, ¡gracias por leer y feliz codificación!

¿Desea crear sus aplicaciones de escritorio, móviles o web con controles de alto rendimiento? Descargue la versión de prueba gratuita de Ultimate ahora y alcance nuevas alturas en el desarrollo de aplicaciones con una gran cantidad de funciones Blazor Biblioteca de componentes, Biblioteca Angular y mucho más.

Ignite UI for Angular beneficios

Solicitar una demostración