Ir para o conteúdo
Simplificando o princípio de substituição de Liskov de SOLID em C#

Simplificando o princípio de substituição de Liskov de SOLID em C#

O Princípio de Substituição de Liskov diz que o objeto de uma classe derivada deve ser capaz de substituir um objeto da classe base sem trazer nenhum erro no sistema ou modificar o comportamento da classe base.

6min read

Antes de começar a escrever este artigo, quero agradecer a Steve Smith por seu ótimo curso sobre o mesmo tópico com o Pluralsight. Este post é inspirado nesse curso.

O Princípio de Substituição de Liskov diz que o objeto de uma classe derivada deve ser capaz de substituir um objeto da classe base sem trazer nenhum erro no sistema ou modificar o comportamento da classe base.

Resumindo: se S é um subconjunto de T, um objeto de T pode ser substituído por um objeto de S sem impactar o programa e trazer qualquer erro no sistema. Digamos que você tenha uma classe Rectangle e outra classe Square. Square é como Rectangle, ou em outras palavras, herda a classe Rectangle. Assim, como afirma o princípio da Substituição de Liskov, devemos ser capazes de substituir o objeto de Retângulo pelo objeto de Quadrado sem trazer nenhuma mudança ou erro indesejável no sistema.

Vamos dar uma olhada mais de perto neste princípio com alguns exemplos.

Understanding the problem

Digamos que temos duas classes, Retângulo e Quadrado. Neste exemplo, a classe Square herda a classe Rectangle. Ambas as classes são criadas conforme listado abaixo:

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

A classe Square herda a classe Rectangle e substitui as propriedades, conforme mostrado na listagem abaixo:

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;
          }
      }
 
  }

Precisamos calcular a área do retângulo e do quadrado.  Para isso, vamos criar outra classe chamada 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;
     }
}

Vamos em frente e escrever testes de unidade para calcular a área do retângulo e do quadrado. Um teste de unidade para calcular essas áreas, conforme mostrado na listagem abaixo, deve ser aprovado.

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

Por outro lado, um teste para calcular a área do Quadrado também deve passar:

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

Em ambos os testes, estamos criando:

1. O objeto do retângulo para encontrar a área do retângulo

2. O Objeto do Quadrado para encontrar a área do Quadrado

E os testes passam conforme o esperado. Agora vamos criar um teste no qual tentaremos substituir o objeto de Rectangle pelo objeto de Square. Queremos encontrar a área de Retângulo usando o objeto de Quadrado e para o teste de unidade para isso está escrito abaixo:

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

O teste acima falharia porque o resultado esperado é 24, no entanto, a área real calculada seria 36.

Este é o problema. Embora a classe Square seja um subconjunto da classe Rectangle, a classe Object of Rectangle não é substituível pelo objeto da classe Square sem causar um problema no sistema. Se o sistema aderiu ao Princípio de Substituição de Liskov, você pode evitar o problema acima.

Resolva um problema com a ausência de herança

Podemos resolver o problema acima seguindo as etapas abaixo:

  1. Livre-se da classe AreaCalculator.
  2. Let each shape define its own Area method.
  3. Em vez de a classe Square herdar a classe Rectangle, vamos criar uma classe base abstrata comum Shape e ambas as classes herdarão isso.

Uma classe base comum Shape pode ser criada, conforme mostrado na listagem abaixo:

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;
    }
}

E a classe Square pode ser reescrita conforme mostrado na listagem abaixo:

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

Agora podemos escrever um teste de unidade para a função de área na classe Rectangle, conforme mostrado na listagem abaixo:

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

O teste acima deve passar sem qualquer dificuldade. Da mesma forma, podemos testar a unidade da função Área da classe Square, conforme mostrado na listagem abaixo:

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

Em seguida, vamos em frente e escrever o teste no qual substituiremos o objeto de Shape pelos objetos de Rectangle e Square.

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]);
}

O teste acima será aprovado e poderemos substituir com sucesso os objetos sem afetar o sistema. No entanto, há um problema com a abordagem acima: estamos violando o princípio aberto-fechado. Cada vez que uma nova classe herda a classe Shape, teremos que adicionar mais uma se a condição estiver no teste e certamente não queremos isso.

O problema acima pode ser resolvido modificando a classe Shape, conforme mostrado na listagem abaixo:

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

Aqui, movemos um método abstrato Area na classe Shape e cada subclasse dará sua própria definição ao método Area. As classes Rectangle e Square podem ser modificadas conforme mostrado na lista abaixo:

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;
   } 
}

Aqui, as classes acima estão seguindo o princípio de Substituição de Liskov e podemos reescrever o teste sem condições "if", conforme mostrado na lista abaixo:

[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]);
}

Desta forma, podemos criar uma relação entre a subclasse e a classe base, aderindo ao Princípio de Substituição de Liskov. As formas comuns de identificar violações dos princípios da LS são as seguintes:

  1. Um método não implementado na subclasse.
  2. A função de subclasse substitui o método da classe base para dar um novo significado.

Espero que você ache este post útil - obrigado por ler e boa codificação!

Quer criar seus aplicativos de desktop, móveis ou da web com controles de alto desempenho? Baixe a avaliação gratuita do Ultimate agora e alcance novos patamares no desenvolvimento de aplicativos com recursos ricos em recursos Blazor Biblioteca de componentes, Angular Biblioteca e muito mais.

Ignite UI for Angular benefícios

Solicite uma demonstração