square is a rectangle

LSP – Is Square A Rectangle?

square is a rectangle

Today we’ll discover that in programming you sometimes end up with surprising conclusions. We’re continuing our journey into the LSP – the Liskov Substitution Principle. Let’s explore the canonical LSP violation: Square / Rectangle.

We have a class Rectangle:

  public class Rectangle
  {
     public int Width { get; set; }
     public int Height { get; set; }
     public int Area => Width * Height;

     public Rectangle(int width, int height)
     {
        Width = width;
        Height = height;
     }
  }

From time to time, our rectangles are squares. We derive a separate class Square from Rectangle:

  public class Square : Rectangle
  { }

Square inherits all its behaviour from Rectangle.

Let’s instantiate a Rectangle 2 units wide and 3 units high.

  Rectangle rectangle = new Rectangle(2, 3);
  rectangle.Width.Should().Be(2);    // passes
  rectangle.Height.Should().Be(3);   // passes 
  rectangle.Area.Should().Be(6);     // passes 

Let’s create a Square with side 2:

  Rectangle square = new Square(2, 2);   // ???
  square.Area.Should().Be(4);            // passes

Here we have the first chink in the armour using Square as a Rectangle: We need to specify width and height on the constructor separately. Let’s fix that with a more suitable Square constructor:

  public Square(int side) 
     : base(side, side) 
  { }

That’s better! Now we can write:

  Rectangle square = new Square(2);

Not so fast. We still have trouble in paradise. Square inherits Rectangle’s Width and Height getter properties:

  var width = square.Width;     // ?? 
  var height = square.Height;   // ?? 

Hold on – squares don’t have separate width and height! They have a side:

  var side = square.Side;

We’re seeing Rectangle’s interface leaking into Square. Width and Height getters worked for Rectangle but are not intuitive for Square. So far, there is no explicit LSP violation – a program using subtype Square in place of Rectangle will work:

  Rectangle square = new Square(2);
  square.Width.Should().Be(2);      // passes 
  square.Height.Should().Be(2);     // passes 
  square.Area.Should().Be(4);       // passes 

Yes, that works. See what happens when we change the Width of the Rectangle:

  Rectangle rectangle = new Rectangle(2, 3);
  rectangle.Width = 4;
  rectangle.Width.Should().Be(4);     // passes 
  rectangle.Height.Should().Be(3);    // passes 
  rectangle.Area.Should().Be(12);     // passes 

Let’s do the same for Square:

  Rectangle square = new Square(2);
  rectangle.Width = 4;
  rectangle.Width.Should().Be(4);    // passes 
  rectangle.Height.Should().Be();    // ??? 
  rectangle.Area.Should().Be();      // ??? 

When we change the Width of a Square – not the Side (!) – what should happen to the Height and Area of the Square? Should the Height also change? Sure, that would be work for Square but is not consistent with how we want to model Rectangle. 

The fundamental problem is that in a geometric (lowercase) square the width and height are linked – squares only have sides. On the other hand, with a (lowercase) rectangle the width and height may vary independently.

The upshot is that class Rectangle is not providing the correct interface and behaviour for class Square. Consequently, we have an LSP violation – the subtype Square cannot be used interchangeably for type Rectangle without altering the operation of the program.

Even though a geometric square is a rectangle, a model of a square is not necessarily a model of a rectangle.

And the solution is? We need to model Square separately from the Rectangle class:

  // Square not derived from Rectangle!
  public class Square
  {
     public int Side { get; set; }
     public int Area => Side * Side ;
     
     public (int side)
     {
        Side = side;
     }
  }

And Square works as expected:

  Square square = new Square(2);
  square.Side.Should().Be(2);     // passes 
  square.Area.Should().Be(4);     // passes 
  square.Side = 3; 
  square.Side.Should().Be(3);     // passes 
  square.Area.Should().Be(9);     // passes 

Great!

One last point: Since a Square no longer derives from Rectangle, we won’t be able to use Square in a program that references Rectangle. And that’s fine – when we were pretending that Square was a Rectangle we ended up violating the LSP and ended up in hot water!

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply