Aside — Careful

Warning

Don’t think of objects as their real world counterparts — that’s a fallacy that OOP promised but failed to deliver.

../../_images/bad_inheritance.png
  • There is nothing wrong with extending concrete classes, but this is where things can become problematic

  • Sometimes taking literal inspiration can be bad

Rectangles and Squares

../../_images/rectangle.png
../../_images/square.png
  • In reality, a square is a special type a rectangle

    • A square is a rectangle where the length and width are equal

  • Below is a simple Rectangle class

 1public class Rectangle {
 2
 3    private double length;
 4    private double width;
 5
 6    public Rectangle() {
 7        this(1,1);
 8    }
 9
10    public Rectangle(double length, double width) {
11        this.length = length;
12        this.width = width;
13    }
14
15    public double getLength() {
16        return length;
17    }
18
19    public void setLength(double length) {
20        this.length = length;
21    }
22
23    public double getWidth() {
24        return width;
25    }
26
27    public void setWidth(double width) {
28        this.width = width;
29    }
30
31    public double getArea() {
32        return length * width;
33    }
34
35    public String toString() {
36        return String.format("Rectangle(Length = %.2f, Width = %.2f)", length, width);
37    }
38}
  • One can also make a Square class that extends the Rectangle class

 1public class Square extends Rectangle {
 2
 3    public Square() {
 4        // Call the superclass' constructor
 5        super();
 6    }
 7
 8    public Square(double side) {
 9        // Call the superclass' constructor
10        super(side, side);
11    }
12
13    public double getSide() {
14        // Could have done getLength
15        return getWidth();
16    }
17
18    public void setSide(double width) {
19        setWidth(width);
20    }
21
22    public String toString() {
23        return String.format("Square(Side = %.2f)", getSide());
24    }
25}
  • This seems great

  • Inherit getLength, getWidth, and getArea

  • Override toString for specific requirements

  • Hijack the superclass’ constructors with super() in a similar way to using this()

Liskov’s Substitution Principle

1public void doubleArea(Rectangle rect) {
2    rect.setWidth(2.0 * rect.getWidth());
3}
  • doubleArea is a method that will double the area of a Rectangle

  • Think of what will happen when running this

1Square mySquare = new Square(10);
2doubleArea(mySquare);
  • Since Square inherits from Rectangle, setWidth exists

  • But this code will cause the Square to have an unequal length and width

    • Thus, the Square is no longer a square

  • This can be fixed by overriding the setWidth (and setLength) methods in the Square class

 1    // Add to Square class to override
 2    // Rectangle's setters
 3    public void setWidth(double width) {
 4        super.setWidth(width);
 5        super.setLength(width);
 6    }
 7
 8    public void setLength(double length) {
 9        this.setWidth(length);
10    }
  • What happens now if we call this?

1Square mySquare = new Square(10);
2doubleArea(mySquare);
  • Now the length and width are equal

  • But this will cause the Square to not double in size, but quadruple, which is a problem

  • This means it is not possible to substitute the Rectangle for a Square for doubleArea

  • However, this can be fixed by changing the doubleArea method

1public void doubleArea(Rectangle rect) {
2    if (rect instanceof Square) {
3        rect.setWidth(Math.sqrt(2.0) * rect.getWidth());
4    } else {
5        rect.setWidth(2.0 * rect.getWidth());
6    }
7}
  • Now this solves it

  • Except, Hyrum’s Law says that all observable behaviours, intentional or not, will be depended on by somebody

  • So, someone out there depends on the fact that doubleArea is quadrupling the Square, even though it honestly shouldn’t be

  • Maybe this can be fixed by adding another method and changing doubleArea back for the person depending on the problematic functionality

 1public void doubleArea(Rectangle rect) {
 2    rect.setWidth(2.0 * rect.getWidth());
 3}
 4
 5public void myDoubleArea(Rectangle rect) {
 6    if (rect instanceof Square) {
 7        rect.setWidth(Math.sqrt(2.0) * rect.getWidth());
 8    } else {
 9        doubleArea(rect);
10    }
11}
  • Now this solves it

  • But, now there is a method saying: if it’s a Square do one thing, if it’s a Rectangle do another thing

    • So… it would seem that here, a Square is not a Rectangle

  • There is also two pieces of code trying to do the same thing

    • What happens if Rectangle gets extended again?

    • Write another version of the method?

  • This ended up requiring a lot of extra work for no reason at all

    • The code got more complex

  • It’s going to be a lot easier to just not use inheritance here

  • If one is truly set on reusing the code, then the better idea here is composition over inheritance

    • Have the Square use an internal instance of a Rectangle to get the desired functionality from Rectangle