I'm trying to nail down a concrete guideline for myself (and maybe others) in this post that is easy to follow.
Ah, I understand you. I have a name for the feeling. I call it The Struggle
I have this friend. He's younger than me. Designer. We have worked together in some projects. He's a very fine individual, very intelligent. But he's got this personality trait: he likes categories. Probably because he's a bit OCD. Everything must be black, or white. Or red. Something. Things need to be categorized following simple, concrete rules.
One of the things he hates to hear the most is "it depends". So we play this game. He starts asking me a technical question. I smile and I answer "It depends", anticipating the "It depends ON WHAT?", which immediately follows. Then I start enunciating the different factors that can affect the outcome. But he loses his patience and he says "So it is impossible to know". And I smile and say "No, it is impossible to know quickly
So now I tell you: It depends
I realize that this is a very unsatisfying answer to a lot of people - as it is for my friend. I will try to expand on the "on what" a little bit.
"What is good programming?" is like "What is good surgery?". First, the definition of "good" is not fixed. Second, there are a multitude of factors affecting the outcome, so the answer can not be categorical - it fluctuates depending on its circumstances. One of the most important factors to me is people.
It would seem that programming is about "making machines do things". And that is correct - but unuseful. It's like saying that "cooking is applying chemical transformations to organic compounds". By leaving the humans out of the context, we lose the context. This is clearly apparent after a software has been running for some time (say, 6 months) and it is out of the "initial version mode". Then it gets into "maintenance". The first bugs are reported. The first change requests are received. There's the need to make changes
in the code. These changes have to be performed by humans. And if the initial was written "just so that the machines do things", making those changes will be extremely difficult.
So I would say that's one of the factors software has to take into account: it has to be easy to change
At the same time, since these changes have to be performed by humans, code needs to be written in a way that is easy for humans to understand it. One way I like to think about it is: Code is not what you tell the machine to do: it is what you tell future programmers that you want the machine to do
. Being "easy to change" is to me more important than "making the machine do what is supposed to do" - after all, if it's easy to change, I can change it to make the thing it is supposed to do.
"Easy to change" and "easy to understand" are not always aligned (discovering this broke my heart a little bit). Let me explain that.
There's this class of things called "dependencies". "Having a dependency" basically means "knowing something about another piece of code". If your code uses a function like love.graphics.print, then your code gains some dependencies: it knows the function name, and it knows the parameters it takes.
Some dependencies are necessary - if we don't depend on love.graphics, we can't use graphics at all. At the same time, if we want software to remain changeable, we must try to minimize dependencies. That is why using globals is very bad: globals are huge dependencies, which make the code more difficult to modify than the alternatives (parameters).
Superclasses are big dependencies as well. You know their names, as well as all their methods, the params each method takes, and their constructor, which is a "special" method. Sometimes the superclass has to leave "hooks" for the subclasses to work, so they "kindof know" what their subclasses are doing.
Mixins are a similar deal: you know their name, their methods and params. They don't have a constructor so in that sense they are a little better than inheritance at dealing with dependencies.
Composition offers a different deal: components themselves have almost no dependencies. The "things using components" (some call these "Entities") have the components as dependencies, but as long as their only role is "being a group of components", and don't add "special extra code", then the dependencies should remain quite manageable.
But we have been talking about "easy to change". What about "easy to understand"?
Well this is more complex because there is some subjectivity involved. What is easy to a senior dev who wrote the whole thing might not be easy for a junior dev who joined the company last week. But still, we can make some assumptions.
Most programmers understand inheritance quite well, because they study it.
Composition is not difficult to understand on itself, but new programmers will have difficulties understanding *why* is used.
Mixins are a bit niche and will take some effort to understand from a lot of programmers, especially new ones.
So, there you have it: depending on what you want to do, and who you are, the answers are different.
In my particular case:
* I use classes most of the time. I don't like inheritance very much, but I do like other aspects of OO, like "creating a bunch of similar entities with custom attributes" or "the decision about what is executed is a responsibility of an object to which you are just sending a message". When I am doing a library I don't use middleclass just to avoid adding a dependency to the lib.
* I try to use inheritance only when I'm certain I will have a very small tree: 2 levels with maybe 1 top class and 3 or 4 subclasses. More than that and I start getting nervous. I sometimes have a base class with very abstract stuff: on the level of draw and delete.
* I try to use composition to deal with "the important logic" of my app. So if I'm doing a videogame, my enemies, player, etc should be entities composed of small, separate components. Most of my libraries are thought to be used as components. The entities and components can be instances of classes. I'm not in the Entity-Component-System bandwagon. I use the dependencies to know how to group stuff together into components. My typical components will have names like "PhysicalBody" or "Projectile". Both the Entities and the Components could be implemented using inheritance, but using many small trees instead of a big-ass one.
* I use mixins when I want to share a non-important bit of logic among several classes (like debugging, logging, or presenting stuff on the screen), when that logic can be expressed as a bunch of functions with no constructor (like "Toggleable" or "Solid"), and when I am too lazy to divide stuff into proper components.
Also note that this set of rules isn't fixed; it depends on me and I change over time.