Sitemap

Square, Rectangle and the Liskov Substitution Principle

5 min readApr 28, 2023

--

Press enter or click to view image in full size

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 and Rectangle inherits from Shape and have their own definitions of the getters and setters and Area() without modifying the expected behavior

Sources

--

--

Alexandre Dutertre
Alexandre Dutertre

Written by Alexandre Dutertre

Student of Holberton School in Laval, France (Cohort #17)

Responses (3)