Does Liskov Substitution Principle (LSP) conflict with public inheritance's IS-A semantics?

Hi,

LSP states that if a function's parameter type is pointer or reference to Superclass, then providing an object from a Subclass should not break the code. For example, Superclass could be Rectangle and Subclass could be Square. This makes sense when public inheritance is supposed to represent an IS-A relationship between the 2 classes. And indeed, we can draw a Venn diagram showing how Square is a subset of Rectangle and this can be coded like so:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Rectangle
{
protected:
  int width, height;
public:
  Rectangle(const int width, const int height)
    : width{width}, height{height} { }

  int get_width() const { return width; }
  virtual void set_width(const int width) { this->width = width; }
  int get_height() const { return height; }
  virtual void set_height(const int height) { this->height = height; }

  int area() const { return width * height; }
};

class Square : public Rectangle
{
public:
  Square(int size): Rectangle(size,size) {}
  void set_width(const int width) override {
    this->width = height = width;
  }
  void set_height(const int height) override {
    this->height = width = height;
  }
};



However the following function proves this relationship wrong.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void process(Rectangle& r)
{
  int w = r.get_width();
  r.set_height(10);

  std::cout << "expected area = " << (w * 10) 
    << ", member function area " << r.area() << std::endl;
}

void unexpected()
{
    Rectangle r{3,4};
    process(r);        // expected area = 30, member function area 30

    Square s{5};
    process(s);        // expected area = 50, member function area 100
}



So, it appears that Square IS-NOT-A rectangle...

Or, these classes are incorrectly formed...



Any ideas?


Juan
The problem is that you never specified what contract an override of Rectangle::set_height() must follow.
If the contract is weak:
1
2
3
If x is an int and r@pre is a Rectangle, then after
r@pre.set_height(x);
r@post.get_height() == x
then process() is incorrect, because it's making stronger assumptions than Rectangle::set_height() provides.
If the contract is strong:
1
2
3
If x is an int, r@pre is a Rectangle, and y == r@pre.get_width() then after
r@pre.set_height(x);
r@post.get_height() == x and r@post.get_width() == y
then a Square is-not-a Rectangle, because its overrides are violating the contracts of the base class.
Last edited on
This is also known as the "circle-ellipse problem", in case anyone's curious.
https://en.wikipedia.org/wiki/Circle–ellipse_problem
Last edited on
So, it appears that Square IS-NOT-A rectangle...


No, class square IS a class rectangle.

What makes a class a rectangle? Being class rectangle, or publicly inheriting from class rectangle. That's it.

You are letting your human conception of what a square and rectangle is in geometry, to influence your understanding of the code. The code doesn't know or care what a square is, what a rectangle is, to a human who happens to think of those words as having special meaning.

The code has simple rules. A "rectangle" is that class named "rectangle", or anything that publicly inherits from that class named "rectangle". That's it.

Any belief you might have about what their width and height values should be are just beliefs you happen to have.

You happen to be using the class "rectangle" and the class "square" to model geometrical objects you know about. You have noticed that the model isn't very good. Doesn't change anything; in the code, a "rectangle" is the class named "rectangle", or anything that publicly inherits from it.

Is this a rectangle class?
1
2
3
4
class rectangle
{
  int number_of_bananas;
};

Yes. You can see right there; "rectangle". Does it have a height? No. Does it have a width? No. Is it still a rectangle class? Yes. Would you say this is NOT class rectangle, because it doesn't contain a height?


What if I do this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class p2
{
protected:
  int rt, k45;
public:
  p2(const int rt, const int k45)
    : rt{rt}, k45{k45} { }

  int get_rt() const { return rt; }
  virtual void set_rt(const int rt) { this->rt = rt; }
  int get_k45() const { return k45; }
  virtual void set_k45(const int k45) { this->k45 = k45; }

  int area() const { return rt * k45; }
};

class a3d : public p2
{
public:
  a3d(int size): p2(size,size) {}
  void set_rt(const int rt) override {
    this->rt = k45 = rt;
  }
  void set_k45(const int k45) override {
    this->k45 = rt = k45;
  }
};


Is there anything in this code that suggests that a3d is NOT a p2 ? There isn't. a3d IS a p2. This code is identical to your previous code, but now the names of the classes doesn't spark extra beliefs in your mind about what their internal values should be, or extra beliefs in your mind about what the internal construction of their functions should be.
Last edited on
Topic archived. No new replies allowed.