Before I start writing this article, I want to thank Steve Smith for his great course on the same topic with Pluralsight. This post is inspired by that course.
The Liskov Substitution Principle says that the object of a derived class should be able to replace an object of the base class without bringing any errors in the system or modifying the behavior of the base class.
In short: if S is subset of T, an object of T could be replaced by object of S without impacting the program and bringing any error in the system. Let’s say you have a class Rectangle and another class Square. Square is as Rectangle, or in other words, it inherits the Rectangle class. So as the Liskov Substitution principle states, we should able to replace object of Rectangle by the object of Square without bringing any undesirable change or error in the system.
Let’s take a closer look at this principle with some examples.
Understanding the problem
Let us say we have two classes, Rectangle and Square. In this example, the Square class inherits the Rectangle class. Both classes are created as listed below:
publicclassRectangle
{
publicvirtualint Height { get; set; }
publicvirtualint Width { get; set; }
}
The Square class inherits the Rectangle class and overrides the properties as shown in the listing below:
publicclassSquare : Rectangle
{
privateint _height;
privateint _width;
publicoverrideint Height
{
get
{
return _height;
}
set
{
_height = value;
_width = value;
}
}
publicoverrideint Width
{
get
{
return _width;
}
set
{
_width = value;
_height = value;
}
}
}
We need to calculate area of the Rectangle and the Square. For this purpose, let us create another class called AreaCalculator.
publicclassAreaCalculator
{
publicstaticint CalculateArea(Rectangle r)
{
return r.Height * r.Width;
}
publicstaticint CalculateArea(Square s)
{
return s.Height * s.Height;
}
}
Let us go ahead and write Unit tests to calculate area of the Rectangle and the Square. A unit test to calculate these areas as shown in the listing below should pass.
[TestMethod]
publicvoid Sixfor2x3Rectangle()
{
var myRectangle = newRectangle { Height = 2, Width = 3 };
var result = AreaCalculator.CalculateArea(myRectangle);
Assert.AreEqual(6, result);
}
On the other hand, a test to calculate area of the Square should also pass:
[TestMethod]
publicvoid Ninefor3x3Squre()
{
var mySquare = newSquare { Height = 3 };
var result = AreaCalculator.CalculateArea(mySquare);
Assert.AreEqual(9, result);
}
In the both tests, we are creating:
1. The Object of Rectangle to find the area of the Rectangle
2. The Object of Square to find the area of the Square
And the tests pass as expected. Now let us go ahead and create a test in which we will try to substitute the object of Rectangle with the object of Square. We want to find area of Rectangle using the object of Square and for the unit test for this is written below:
[TestMethod]
publicvoid TwentyFourfor4x6RectanglefromSquare()
{
Rectangle newRectangle = newSquare();
newRectangle.Height = 4;
newRectangle.Width = 6;
var result = AreaCalculator.CalculateArea(newRectangle);
Assert.AreEqual(24, result);
}
The above test would fail, because the expected result is 24, however the actual area calculated would be 36.
This is the problem. Even though the Square class is a subset of the Rectangle class, the Object of Rectangle class is not substitutable by object of the Square class without causing a problem in the system. If the system adhered to the Lisokov Substitution Principle, you may avoid the above problem.
Solve problem with No-Inheritance
We can solve the above problem by following the below steps:
1. Get rid of the AreaCalculator class.
2. Let each shape define its own Area method.
3. Rather than Square class will inherit Rectangle class, let us create a common abstract base class Shape and both classes will inherit that.
A common base class Shape can be created as shown in listing below:
public abstractclassShape
{
}
Next, the Rectangle class can be rewritten as follows:
publicclassRectangle :Shape
{
public int Height { get; set; }
public int Width { get; set; }
publicint Area()
{
return Height * Width;
}
}
And the Square class can be rewritten as shown in the listing below:
publicclassSquare : Shape
{
publicint Sides;
publicint Area()
{
return Sides * Sides;
}
}
Now we can write a unit test for the area function in the Rectangle class as shown in listing below:
[TestMethod]
publicvoid Sixfor2x3Rectangle()
{
var myRectangle = newRectangle { Height = 2, Width = 3 };
var result = myRectangle.Area();
Assert.AreEqual(6, result);
}
The above test should pass without any difficulty. In the same way we can unit test the Area function of the Square class as shown in the listing below:
[TestMethod]
publicvoid Ninefor3x3Squre()
{
var mySquare = newSquare { Sides = 3 };
var result = mySquare.Area();
Assert.AreEqual(9, result);
}
Next let us go ahead and write the test in which we will substitute object of Shape with the objects of Rectangle and Square.
publicvoid TwentyFourfor4x6Rectangleand9for3x3Square()
{
var shapes = newList<Shape>{
newRectangle{Height=4,Width=6},
newSquare{Sides=3}
};
var areas = newList<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]);
}
The above test will pass and we are successfully able to substitute the objects without impacting the system. However there is one problem in the above approach: we are violating the open closed principle. Each time a new class inherits the Shape class, we will have to add one more if the condition is in the test and we certainly do not want this.
The above problem can be solved by modifying the Shape class as shown in the listing below:
public abstractclassShape
{
publicabstractint Area();
}
Here we have moved an abstract method Area in the Shape class and each sub class will give its own definition to the Area method. Rectangle and Square class can be modified as shown in the listing below:
publicclassRectangle :Shape
{
public int Height { get; set; }
public int Width { get; set; }
publicoverrideint Area()
{
return Height * Width;
}
}
publicclassSquare : Shape
{
publicint Sides;
publicoverrideint Area()
{
return Sides * Sides;
}
}
Here the above classes are following the Liskov Substitution principle, and we can rewrite the test without “if” conditions as shown in the listing below:
[TestMethod]
publicvoid TwentyFourfor4x6Rectangleand9for3x3Square()
{
var shapes = newList<Shape>{
newRectangle{Height=4,Width=6},
newSquare{Sides=3}
};
var areas = newList<int>();
foreach (Shape shape in shapes)
{
areas.Add(shape.Area());
}
Assert.AreEqual(24, areas[0]);
Assert.AreEqual(9, areas[1]);
}
In this way we can create relationship between the sub class and the base class by adhering to the Liskov Substitution principle. Common ways to identify violations of LS principles are as follows:
1. Not implemented method in the sub class.
2. Sub class function overrides the base class method to give it new meaning.
I hope you find this post useful – thanks for reading and happy coding!
Want to build your desktop, mobile or web applications with high-performance controls? Download Ultimate Free trial now and see what it can do for you!