Classes are not exactly abstractions, they are concretions (potentially of an abstraction). Generally, whatever you can create an instance of typically:
- Is concrete.
- May have specific state.
- May have specific behavior.
It might be that you are confusing the class definition and the class instances. Of course class definitions are an abstraction that represents the range of all possible instances (defined as the set of all instances occurring by cover the entire domain of the internal state parameters).
Your Dog::Collie:Lassie
example showcases a minor problem. Dog
and Collie
are class definitions, but Lassie
should be a specific class instance. It is a just matter of taxonomy, really. Collie
is a Type
, but Lassie
is a Name. So, unless you really want to represent Lassie
as a Type (in which case you may have multiple Lassie instances).
To make this clearer, let me introduce you to my two friends. I have 3 instances of Michael and another 3 instances of Jordan:
Michael morningMichael = new Michael();
Michael afternoonMichael = new Michael();
Michael nightMichael = new Michael();
Jordan morningJordan = new Jordan();
Jordan afternoonJordan = new Jordan();
Jordan nightJordan = new Jordan();
Now, this is my mental model. When I see Michael in the afternoon, it is a different Michael, who happens to bear terrific resemblance to the morning version of Michael. He is a little more tired than morning Michael, his weight is slightly different, and stuff, but he still has two hands, two legs, one head, same hair color, same complexion, etc. There are two obvious problems with this mental model, and one that is not so obvious.
- There is gross definition duplication. Michael and Jordan share very similar traits so I will have to code definitions multiple times. I can overcome this by using base definitions and inheriting, but I will reach a point where, after inheriting, there will be virtually nothing more to define.
- There is no resemblance of this mental model to reality, which is what I have been trained in for my entire life, so reality is a model I can perfectly, easily and fluently relate to, and whatever I can translate to or from reality is, practically, easier for me to understand, grasp and develop around. When I see a friend after a day goes by, I don't ever feel like it is a copy of my friend...
- The not so obvious problem is that this is perfectly allowed using available coding tools and definition/prototyping capabilities.
In solving the third problem, at some point, Types must end, and State must come to be. Therefore, I choose to stop my definitions (types, really) at Human
and let state manifest, in the form of instances. Then, whenever I read my code, it really takes me less than a few milliseconds to understand that whenever I come across instance micheal, it is really the same Human
I came across earlier, when talking about michael. He may have changed internally, but it is the same guy (viz. the same byte-group in memory).
Human michael = new Human();
Human jordan = new Human();
So, like I said earlier, it is a matter of taxonomy, really. It is more like philosophy, but also a lot about what makes you more productive. Object-oriented programming borrows from the real world, because understanding the real world is something we all have a lot of training in, and we can express our ideas much easier in that model.
All that aside, the extent of detail one reaches is always dependent on the case. For example, while birds are, technically, dinosaurs, you will not come across many real-world code-bases with a hierarchy such as Dinosaur::Bird...
. So, as usual, more often than not, your very own definitions in your code generally reflect what you are trying to achieve, and this is what you should use to balance your choices.