Humans love to build hierarchies of items, such that an items clearly fits within a tree of knowledge. Examples of this include the various Taxonomic ranks of Biology (there are competing ones) https://en.wikipedia.org/wiki/Kingdom_(biology)
As the early programmer that designed object-orientation was a Biologist, he added in a type system that supported this idea. Everything was an "Object" and items were cleanly divided into smaller groupings based on the programmer's organizational model.
The problem with this is the same problem that exists in Biology. Often there are items that must span very large branches. They tend to fit "somewhat" in at least two (or more) branches of the tree. This leaves the programmer with a few choices:
- Make a new tree branch and treat the object as an oddity.
- Put it in the best fitting branch, and try to handle the bits that don't fit with odd workarounds.
- Restructure the tree.
The last choice is the best from a design point of view, but the hardest to actually achieve, as everyone who encountered your organization will then have to be updated to the new organization. When dealing with programming, this means all dependent modules need to be updated / rewritten.
With time, you will realize that the hierarchies are useful, but they shouldn't be organized by "what the item is" but rather by "what the item can do", and by clearly indicating that an item can live in many "functional, as in what it can do" branches. This is why many will caution against long lists of derived classes, where one class is derived from another. It's an improvement on the original ideas laid down in the 1950's / 1960's.
So the best practice is a bit at odds with most early texts that support a "a triangle is a shape, that's a object", or "a dog is a pet that's an animal that's an object". Instead you need to look at your program to decide the hierarchy.
"a triangle is an area giver and a side giver and a list of drawable items giver" will have no trouble in adding new drawable items, items with areas, or items which count sides. Oddly enough, if you opt for "a triangle is a closed shape is a shape is an object" you might find that by placing drawing routines in shape, you could complicate the inclusion of circles, as you probably built the drawing routines to handle lines. Fixing them in this hierarchy means you now need to update triangles so they can inherit the drawing of curved lines, which is odd and the primary reason one now recommends very short inheritance chains, with the upper level names being mostly interfaces (so you can add many of them) avoiding updates to classes that don't participate it whatever new functionality is being added to a traditional type tree.