Aside — Careful
Warning
Don’t think of objects as their real world counterparts — that’s a fallacy that OOP promised but failed to deliver.
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
A classic example used for teaching inheritance is squares and rectangle
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
Rectangleclass
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
Squareclass that extends theRectangleclass
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, andgetAreaOverride
toStringfor specific requirementsHijack the superclass’ constructors with
super()in a similar way to usingthis()
Liskov’s Substitution Principle
-
This is the “L” in the SOLID design principals
1public void doubleArea(Rectangle rect) {
2 rect.setWidth(2.0 * rect.getWidth());
3}
doubleAreais a method that will double the area of aRectangleThink of what will happen when running this
1Square mySquare = new Square(10);
2doubleArea(mySquare);
Since
Squareinherits fromRectangle,setWidthexistsBut this code will cause the
Squareto have an unequal length and widthThus, the
Squareis no longer a square
This can be fixed by overriding the
setWidth(andsetLength) methods in theSquareclass
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
Squareto not double in size, but quadruple, which is a problemThis means it is not possible to substitute the
Rectanglefor aSquarefordoubleAreaHowever, this can be fixed by changing the
doubleAreamethod
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
doubleAreais quadrupling theSquare, even though it honestly shouldn’t beMaybe this can be fixed by adding another method and changing
doubleAreaback 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
Squaredo one thing, if it’s aRectangledo another thingSo… it would seem that here, a
Squareis not aRectangle
There is also two pieces of code trying to do the same thing
What happens if
Rectanglegets 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
Squareuse an internal instance of aRectangleto get the desired functionality fromRectangle