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
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 theRectangle
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
, andgetArea
Override
toString
for 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}
doubleArea
is a method that will double the area of aRectangle
Think of what will happen when running this
1Square mySquare = new Square(10);
2doubleArea(mySquare);
Since
Square
inherits fromRectangle
,setWidth
existsBut this code will cause the
Square
to have an unequal length and widthThus, the
Square
is no longer a square
This can be fixed by overriding the
setWidth
(andsetLength
) methods in theSquare
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 problemThis means it is not possible to substitute the
Rectangle
for aSquare
fordoubleArea
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 theSquare
, even though it honestly shouldn’t beMaybe 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 aRectangle
do another thingSo… it would seem that here, a
Square
is not aRectangle
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 aRectangle
to get the desired functionality fromRectangle