Square, Rectangle and the Liskov Substitution Principle
When creating classes for shapes, it’s easy to imagine a square as just being a rectangle with all sides having the same length, like in geometry. Thus, making the Square
class inherits from the Rectangle
class. However, this could bring unexpected behavior from our program, as it violates the Liskov substitution principle (LSP). We will see how we can fix that problem.
What is the Liskov substitution principle
The Liskov substitution principle is the third principle of the mnemonic acronym SOLID:
- Single-responsability principle
- Open-closed principle
- Liskov substitution principle
- Interface segregation principle
- Dependency inversion principle
It was introduced by Barbara Liskov, during a conference called Data abstraction and hierarchy, in 1987. This principle defines the subtype notion as such:
if f(x) is a property provable about objects x of type T, then f(y) should be true for objects y of type S where S is a subtype of T.
An objet can be replaced by a sub-object without breaking the program, as what holds for T-objects holds for S-objects is what must be understood.
A square breaks that principle since it doesn’t have a different height
and width
. So not everything true for Rectangle
is true for Square
. Let’s see it using code and the Shape
, Rectangle
and Square
classes.
Shape class
class Shape
{
public virtual int Area()
{
throw new NotImplementedException("Area() is not implemented");
}
}
Rectangle class
class Rectangle : Shape
{
private int width;
private int height;
public int Width
{
get => width;
set
{
if (value < 0)
throw new ArgumentException("Width must be greater than or equal to 0");
width = value;
}
}
public int Height
{
get => height;
set
{
if (value < 0)
throw new ArgumentException("Height must be greater than or equal to 0");
height = value;
}
}
public new int Area() => width * height;
public override string ToString() => $"[Rectangle] {width} / {height}";
}
Square class
class Square : Rectangle
{
private int size;
public int Size
{
get => size;
set
{
if (value < 0)
throw new ArgumentException("Size must be greater than or equal to 0");
size = value;
Height = value;
Width = value;
}
}
public override string ToString() => $"[Square] {size} / {size}";
}
If we were to use the setter Size
, the program would still work as intended. However if we use directly the setters Width
and Height
with our square, it would break the program.
class Program
{
static void Main(string[] args)
{
Square aSquare = new Square();
try
{
aSquare.Width = 12;
aSquare.Height = 8;
Console.WriteLine("aSquare width: {0}", aSquare.Width);
Console.WriteLine("aSquare height: {0}", aSquare.Height);
Console.WriteLine("aSquare size: {0}", aSquare.Size);
Console.WriteLine("aSquare area: {0}", aSquare.Area());
Console.WriteLine(aSquare.ToString());
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
}
Here is the output obtained when running the dotnet run
command:
aSquare width: 12
aSquare height: 8
aSquare size: 0
aSquare area: 96
[Square] 0 / 0
We can see that there is a problem when getting the Size
, using the Area()
and ToString()
methods since the field size
was never assigned. We’re using the methods as if we were expecting a rectangle, aside from aSquare.Size
, but it behaves differently since it’s a square.
Overriding the getters and setters isn’t an option since it modifies the behavior of the methods from the Rectangle
class which violates the LSP.
Possible correction
One possible correction to this problem, would be to make the Shape
class abstract
and make both Rectangle
and Square
inherits from the Shape
class. Here is a possible implementation:
Shape class
abstract class Shape
{
protected int width;
protected int height;
public abstract int Area();
}
Rectangle class
class Rectangle : Shape
{
public int Width
{
get => width;
set
{
if (value < 0)
throw new ArgumentException("Width must be greater than or equal to 0");
width = value;
}
}
public int Height
{
get => height;
set
{
if (value < 0)
throw new ArgumentException("Height must be greater than or equal to 0");
height = value;
}
}
public override int Area() => width * height;
public override string ToString() => $"[Rectangle] {width} / {height}";
}
Square class
class Square : Shape
{
public int Size
{
get => width;
set
{
if (value < 0)
throw new ArgumentException("Size must be greater than or equal to 0");
width = value;
height = value;
}
}
public int Width
{
get => width;
set
{
if (value < 0)
throw new ArgumentException("Width must be greater than or equal to 0");
width = value;
height = value;
}
}
public int Height
{
get => height;
set
{
if (value < 0)
throw new ArgumentException("Height must be greater than or equal to 0");
width = value;
height = value;
}
}
public override int Area() => width * width;
public override string ToString() => $"[Square] {width} / {width}";
}
If we modify main.cs
to add output for a rectangle:
class Program
{
static void Main(string[] args)
{
Square aSquare = new Square();
Rectangle aRectangle = new Rectangle();
try
{
aSquare.Width = 12;
aSquare.Height = 8;
aRectangle.Width = 12;
aRectangle.Height = 8;
Console.WriteLine("aSquare width: {0}", aSquare.Width);
Console.WriteLine("aSquare height: {0}", aSquare.Height);
Console.WriteLine("aSquare size: {0}", aSquare.Size);
Console.WriteLine("aSquare area: {0}", aSquare.Area());
Console.WriteLine(aSquare.ToString());
Console.WriteLine("------------------------");
Console.WriteLine("aRectangle width: {0}", aRectangle.Width);
Console.WriteLine("aRectangle height: {0}", aRectangle.Height);
Console.WriteLine("aRectangle area: {0}", aRectangle.Area());
Console.WriteLine(aRectangle.ToString());
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
}
We obtain the following output.
aSquare width: 8
aSquare height: 8
aSquare size: 8
aSquare area: 64
[Square] 8 / 8
------------------------
aRectangle width: 12
aRectangle height: 8
aRectangle area: 96
[Rectangle] 12 / 8
The Shape
class is made abstract
as by itself it doesn’t refer to anything concrete, and abstract
classes are an important part in this principle. The Rectangle
and Square
classes inherits from this class as they are shapes.
By separating the Rectangle
and Square
classes, we ensure that there are no problems during the code execution as the classes holds their own implementation of the Area()
method and their own definition for Width
and Height
.
Overriding isn’t a problem here, because it doesn’t modify the expected behavior for Area()
, which is to return the area. That’s why it was made abstract
in the Shape
class, to indicate it must be overridden.
Conclusion
- Contrary to geometry, squares aren’t rectangles in Object-Oriented Programming
- Subclasses must respect the Liskov substitution principle
- The LSP dictates that everything true for the class, must be true for the subclass
- The problem can be fixed by making
Square
andRectangle
inherits fromShape
and have their own definitions of the getters and setters andArea()
without modifying the expected behavior