Actually, my decision on abstracting is made on the basis if the abstraction makes semantically sense, not just if there is some code in common. Because of this, my code usually has class hierarchies that are one, maximum two, level deeps and in most cases the first level is just an interface, declaring the operations I expect from the descendant of that root class. I use this solution especially for those parts of my code that are plugin-based, for example, a program that read some external data could define a "reader interface" and I would be able to add a new input format by just adding a new descendant (plug-in) of the reader interface.
For example, I am currently writing a software that helps in writing European research proposal. In a research proposal you have several "entities:" partners, workpackages, tasks, deliverables and milestones.
Every entity has a name, a short name (used when there is not much space) and a label (used for cross-reference). Deliverables and milestones also have a date when they are expected, while WPs and tasks have beginning and ending dates.
Therefore, my hierarchy is something like this
entity (abstract, it has name, short name and label)
+-- event-like entity (abstract, something that happens at a given time)
| +-- deliverable
| +-- milestone
+-- activity-like entity (abstract, begin and end time)
+-- work package
Note that I never talked about code, the reason for doing this abstraction are basically of "semantic" type. Note also that the abstraction was born from the "concrete" classes and recognizing some commonality in their semantic. It was not born by starting from a "grand root abstract class" with nothing in it, whose only goal is to have everything descends from it.
Totally agree with your view on what makes abstraction worthwhile.
A one-to-two level deep inheritance or composition is most ideal since it doesn't suffer from Chinese Whispers effect. I often refer to the change as an identity corruption of code, as keeping the identity of a function/class true to its origin would keep it readable and maintainable.
We're a place where coders share, stay up-to-date and grow their careers.
We strive for transparency and don't collect excess data.