I think we should start from what your definition of "good code" is. At the most basic practical level we can ask:
a) Does the program logically do what I want. Or what some spec. demands?
b) Does the program not do what I don't want it to?
c) Does it do all that with the performance required?
d) Often for me, does it fit in the memory available?
If the answer to all that is "yes" then it is good code.
After that we get into some very nebulous territory:
e) Is it understandable to those who may come later to fix/extend/port it. Or even myself later?
d) Is it easily extendable, modifiable, portable?
Here we get into a swamp of advice about DRY, SOLID, Design Patterns etc. Countless books on the topics.
The problem is, once you get past the requirements of the here and now you are contemplating some possible requirements in the future. Which is of course very unpredictable.
I have come to the conclusion that nobody knows. No matter what they claim. For example Object Oriented Programming was de rigueur for a couple of decades. Today there is a tangible rebbelion against it.
When I started out "top down", structured design and programming you describe was the way to go: Understand, split, design, code. For a long time now overpaid consultants have been touting "agile" development instead.
I gave up hope of being a "good" programmer decades ago. It's impossible to keep up with all the fads in real-time. I just make stuff work. If I can do it so that the code reads like a coherent novel all the better. Of course I follow company standards, conventions, etc when I have to.
Bottom line is. Learning by doing. Learning by reading what others have done. It really helps if you can get to work with other skilled, experienced, programmers. In one second they can explain why doing this thing is better than doing that thing, usually by pulling apart code you have put up for review. All what they say may well be in books somewhere, but you may never find it or not understand the significance if you do.
My current advice is:
-
If at all possible split things up into functions/modules even classes that are meaning full and possibly useful by themselves
-
Which implies minimizing coupling between those functions/modules/classes.
-
Minimize the amount of comments you write. Comments are extra maintenance work, change the code you need to change the comment. Which means they often fall out of sync with reality and are wrong. More confusing than helpful. The compiler cannot check them.
When you find yourself writing comments ask yourself why? If you find yourself writing something like:
// Read widget.
// Loads of code...
// Process widget data
// Loads of code...
// Write other widget
// Loads of code...
That may already be a clue that you have big chunks of code that could be pulled out into meaning full functions/methods
- Give things sensible names. For example those steps above could become "read_widget(widget)" or widget.read() or whatever. At that point the way you have created and named the functions already says what you previously wrote in comments. Make the comments redundant.
Naming things meaningfully is said to be one of the hardest things in computer science. I find that when I start thinking about what a thing should be called, different name choices change how I think about what the thing really is, or what it should be.
OK. Enough of my rambling.