Hackle's blog
between the abstractions we want and the abstractions we get.

# A Square IS A Rectangle - If Designed Correctly

An age-old example from Object-Oriented literature for the classic Liskov Substitution Principle is the much acclaimed "A square is not a rectangle".

It goes roughly as, `Square` is a sub-type of `Rectangle`, just as the intuition that a square is a special rectangle with equal height and width. In plain simple TypeScript,

``````class Rectangle0 {
constructor(public width: number, public height: number) {}
}

class Square0 extends Rectangle0 {
constructor(side: number) {
super(side, side)
}
}
``````

For dramatic effect, here we say "so far so good, but..." the intuition breaks down when we try to set `Square.height`, the `Square` is no longer a `Square`. Bummer!!!

``````const square0 = new Square0(5);
// Oh NO! height != width This is no longer a Square
square0.height = 4;
``````

A more sophisticated version invests generously in setters/accessors. I'll spare you the ugly code listing and show only the gist in the `Square` sub-type.

``````class Square1 extends Rectangle1 {
// same goes for: override set width(value: number)
override set height(value: number) {
super.height = super.width = value;
}
}
``````

This looks reasonable for the `Square`, but is surprising if we consider the base class - `Square`'s behaviour deviates from that of `Rectangle`, whose `width` and `height` are expected to change independently. As LSP points out, a sub-type should satisfy ALL expectations set out by the base class, both in appearance and in spirit!

Here Object-Oriented literature laments the violation of the mathematical intuition, but not without joy in pointing out the moral of the lesson: that software design is NOT always what you think. No sir, it's a sophisticated and arduous endeavour, definitely not for the faint-hearted.

Sigh, oh well, a Square Is Not a Rectangle!

Oh but hold on! It's time we put a stop to such nonsensical teaching based on a terribly broken example, kept in centre stage for decades with the perpetuation of questionable mainstream thinking.

Let's bring some sanity by resorting to common sense: if I change the width (and width only) of a square, should I still get a square? Think hard before jumping to any answers.

Of course not! I get a rectangle!

The height of the shape should not change automagically with the width - it only does so in badly designed software with false assumptions.

The right behaviour is not hard to implement either, if we pay a little respect to common sense, and renounce one or two die-hard habits.

``````class Rectangle2 {
}

setHeight(height: number): Rectangle2 {
return new Rectangle2(this.width, height);
}
}

class Square2 extends Rectangle2 {
super(side, side);
}
}

const rect2 = new Rectangle2(3, 8);
const square2 = new Square2(4);

// Hello! It's a Rectangle, not a Square
const rect3 = square2.setHeight(5);
``````

The gist is with `setHeight(height: number): Rectangle`,

1. setting the `height` of (or "stretching") a `Rectangle`, including the special case of `Square`, will result in another `Rectangle`.
2. calling `setHeight` on a `Square` should NEVER magically set its `width`. In fact, automagically setting `width` when the caller calls `setHeight`, is the smacking violation of LSP.

Now let's zoom in. Do you see what die-hard habits I was alluding to that should be "renounced"? Let me spell it out,

Mutation! We prohibit the likes of `Square.height = 5` by making all fields `readonly`. Now `Square` is immutable and must stay as constructed. Therefore, it is a much stronger type, whose values cannot be (at least not easily) manipulated out of shape (pun intended).

If we go a bit further, another source of evil is the entrenched teaching to mix data and behaviour, which hopefully is quickly going out of favour: more modern languages such as Rust and Go encourage us to keep data and behaviour separate, while offering a flavour of dot notation similar to that of classes.

Hope we are now on the same page on the age-old nonsense, "A square is not a rectangle". Quite the broken example steeped in the intrinsic shortcoming of the unfortunate combination of classes and mutation, and it does 0 justice to the principle by the great Babara Liskov.

PS. with all that said about the solution, I am not stating that this problem is meaningless and should be discarded for good. Instead, it can be a great exercise if presented as a design challenge: how do we model `Square` and `Rectangle` so their behaviours are inline with our mathematical understanding?

This way, we can now pose questions such as: what if I want change the size of a `Square` to get another `Square`?

And the answer may be: you must construct a new `Square`. Why? Because we choose to design it so that the constraint of "equal width and height" is enforced through the constructor. Free mutation of width and height side-steps the constructor, therefore must be disallowed to ensure correctness.