I always find that the common example of Rectangles and Squares leads me to a different conclusion. It assume that the best way to go about is to only have a single sideLength method, but from a data structure perspective it seems more obvious that the Squares class substitute instead the validation method by adding constraints that a valid square only exist when both sides are equal. Rectangle class must already have a validation method that check that each side is greater than zero, so it seem like the obvious place to define a square. Any additional methods or properties like sideLength would just be added convenience and optimization to access the width and height at the same time, but which is not required for anyone substituting one for the other.
But then both are just a kind of polygon, with an arbitrary number of sides of arbitrary length.
At which point you really want a variable length list of Sides, each with a length property.
Or you want a series of points with relative coordinates, making the sides implicit.
Or or or...
What I take from it is that there is no one true object model, there is no universally "correct" way of solving the problem. An evolving understanding of the problem leads to an evolving solution that places different priorities on different attributes.
Is there any value provided to the system by making Square a specialisation of Rectangle? Is there value in being able to express a Square without a redundant attribute value? What is the tradeoff? And so on.
It's interesting because it highlights that if a thing has so many properties describing what it is, then at a certain point it's not a thing at all, but a collection of characteristics that could be tweaked to describe anything.
At that point, the idea of behaviors is rendered void, as the universe of possible behaviors is just too large.
So, I think that's the guiding principle in your suggestion to consider what value a given design provides to the system: that is, start by modelling the behaviors your system requires, then create an object hiearchy that reflects those behaviors most concisely.
This is probably our intent, but we may be derailed by too little respect for YAGNI.
This is a very insightful point. These models and concepts are all human created abstractions.
They contain elements of arbitrariness, describe different levels of detail, focus in on certain aspects or dynamic, leave out other information, and so on.
The skilled software developer must determine how useful and well-suited any particular abstraction is for the actual problem at hand, and judiciously move forward that.
As understanding of the problem domain grows, the abstractions should be updated and discarded to reflect this.
Pragmatism is key -- what does the system actually need to do? Supposed platonic ideals of some conceptual object are often a wild goose chase. Reality is complex. We must embrace it.
The problem is that the square is a rectangle in the mathematical sense: a rectangle with ANY additional constraints while code which uses a rectangle expects a rectangle with NO further constraints.
I believe the right choice is to have an AbstractRectangle class representing the rectangle with any further constraints and Rectangle class representing the rectangle with no further constraints.
As niklasjansson points out, this solution breaks Liskov substitution. Squares programmed in such a way are no longer substitutable for rectangles. Breaking Liskov substitution is a great way to hide bugs and make your codebase extremely hard to reason about, due to the fact that every subtype can have drastically different behavior than what's defined by the parent type.