Rectangle r = new Rectangle(height = 3, width = 5);
Square s_as_s = new Square(side = 4);
Rectangle s_as_r = s_as_s;
print(r.height); // prints 3
print(r.width); // prints 5
print(s_as_s.height); // prints 4
print(s_as_s.width); // prints 4
print(s_as_r.height); // prints 4
print(s_as_r.width); // prints 4
print(r is_a? Square); // prints false
print(s_as_s is_a? Square); // prints true
print(s_as_r is_a? Square); // prints true
Okay, so the question comes up when you mutate these results: r.height = 2;
s_as_r.height = 2;
Keep in mind, this is a perfectly reasonable thing to do in both cases: you're just making two rectangles a little shorter. But no matter how you handle this situation, the results are surprising:One way:
print(r.height); // prints 2
print(r.width); // prints 5
print(s_as_s.height); // prints 2
print(s_as_s.width); // prints 2
print(s_as_r.height); // prints 2
print(s_as_r.width); // prints 2
print(r is_a? Square); // prints false
print(s_as_s is_a? Square); // prints true
print(s_as_r is_a? Square); // prints true
This is the simplest to implement, but only because there's a part of the contract of Rectangle which is implied and not enforced by the compiler. When we change the height of a rectangle, we don't expect the width to change. This is the sort of gotcha that needs to be put in the documentation in big red letters: "WARNING: CHANGING THE HEIGHT MAY CHANGE THE WIDTH IN SOME SITUATIONS."Another way:
print(r.height); // prints 2
print(r.width); // prints 5
print(s_as_s.height); // prints 2
print(s_as_s.width); // prints 4
print(s_as_r.height); // prints 2
print(s_as_r.width); // prints 4
print(r is_a? Square); // prints false
print(s_as_s is_a? Square); // prints true
print(s_as_r is_a? Square); // prints true
But now you've broken the contract of Square: the user is going to be very surprised when changing the side of one side of a Square means that the Square instance no longer represents a square.Okay, what about this:
print(r.height); // prints 2
print(r.width); // prints 5
print(s_as_s.height); // prints 2
print(s_as_s.width); // prints 4
print(s_as_r.height); // prints 2
print(s_as_r.width); // prints 4
print(r is_a? Square); // prints false
print(s_as_s is_a? Square); // prints false
print(s_as_r is_a? Square); // prints false
This might be possible with some horrible hack in a language that does dynamic typing. This maintains the contracts of all the types, but I'd argue that it breaks the contract of the language itself: it's deeply confusing to have the type of the s_as_s variable change out from under you.Perhaps you could do this:
print(r.height); // prints 2
print(r.width); // prints 5
print(s_as_s.height); // prints 2
print(s_as_s.width); // prints 2
print(s_as_r.height); // prints 2
print(s_as_r.width); // prints 4
print(r is_a? Square); // prints false
print(s_as_s is_a? Square); // prints true
print(s_as_r is_a? Square); // prints false
Setting aside how one might even implement this, we've now got the surprising result that s_as_s and s_as_r seem to be different instances now.Maybe we should have prevent this in the first place:
r.height = 2; // works
s_as_s.height = 2; // throws CannotSetSideViaHeightException
s_as_r.height = 2; // throws CannotSetSideViaHeightException
s_as_s.height = 2 throwing an exception sort of makes sense, but now s_as_r isn't behaving like a rectangle--we're breaking the Rectangle contract again.Another way to prevent it:
r.height = 2; // works
s_as_s.height = 2; // works
s_as_r.height = 2; // throws ThisWouldBeConfusingException
Again you're breaking the contract of the language, that s_as_s and s_as_r seem to be different objects.There's only one way left I can think of to make Rectangle maintain its contract, Square maintain its contract, and keep s_as_s and s_as_r behaving the same way:
r.height = 2; // throws MutationException
s_as_s.height = 2; // throws MutationException
s_as_r.height = 2; // throws MutationException
Does this look familiar? It should: it's basically immutability implemented as checks at run time, which is not a good way to implement it. At this point we should just implement these as immutable objects.I'm not necessarily saying that immutability is the only way. You could also give up inheritance:
Rectangle makeSquare(int side) {
return new Rectangle(side, side);
}
Rectangle r = new Rectangle(3, 5);
Rectangle s_as_r = makeSquare(4);
print(r.isSquare); // prints false
print(s_as_r.isSquare); // prints true
r.height = 2;
s_as_r.height = 2;
print(r.height); // prints 2
print(r.width); // prints 5
print(s_as_r.height); // prints 2
print(s_as_r.width); // prints 4
print(r.isSquare); // prints false
print(s_as_r.isSquare); // prints true
This also results in unsurprising behavior.