A few days ago I found two developers at Simpl discussing how to be a good programmer. I participated in the discussion and figured writing down my thoughts here would be helpful to some.
Programming is a very unique activity in the spectrum of human activities. On the one hand there are activities which are done purely for the love it - they are artistic activities, like creating music, or doing painting. While some musicians or painters do become famous and rich, music or painting in itself is not something which directly brings economic gains to the society in the way that a loaf of bread (which can be eaten) or a brick (which is used to build house) does. Musicians and painters, at least the best of them, practice their art for the love of it.
A lot of Mathematics falls in this domain too. While Mathematics has a lot of practical uses, mathematicians push the boundaries of human knowledge for its own sake, and it is lucky coincidence that advanced Mathematics is sometimes useful in other areas of human activity, particularly Physics. As an aside, A Mathematician's Apology is a good book to explore thoughts on pure vs applied Mathematics.
On the other hand, there are mundane activities like business and politics. People don't do business to practice business for its own sake, their goal is to make money. Politicians don't do politics for its own sake, their goal is mostly power, money and sometimes hopefully to serve the country.
Programming is unique in the sense that while we don't write program just for the sake of programming, but still programming is an art - the feeling of joy that you get when you write a good program is similar to the joy you get when you create good music or painting or prove a nice theorem, and it is unlike the joy that you get when you succeed in business or politics. There is something in programming that is soothing to the soul.
I don't have a strong intuition about where to place other engineering activities in this spectrum - is designing a car (mechanical engineering), or a rocket (aerospace engineering) also an artistic activity? May be, but my intution is that software engineering is just more artistic than other, more physical branches of engineering.
So, software engineers are in a unique position - they can do something artistic while producing something which is directly useful to the society. This also makes their position precarious. If they focus only on the art part of it, then in a corporate setting they would be too slow and that would harm them. If they focus only on the delivery part of it, forgetting art completely then in the long term they would remaing poor programmers and that would hurt delivery part of their work.
The struggle to become a good programmer is that of appreciating and honing the art part of programming more and more (which brings business benefits in the long term, and makes programming more pleasing) while doing a good job on day to day delivery.
Alright, with this preamble, let's get to the topic of the article - how to become a good programmer. Here is a potpourri of suggestions.
Learning a variety of programming languages gives you different perspectives: Java is object oriented, in Python you can combine functional style with object oriented. C is close to hardware. So, learning different languages improves your appreciation of language features.
Particularly interesting is learning "pure" languages. For instance Haskell is pure functional language and Smalltalk is a purely object oriented language. Most languages allow you to mix programming styles and thus it is difficult to understand the essence of a certain programming style. By working in a pure language you get handicapped in that you cannot unknowingly use other style and thus you understand a given style better.
There are two kinds of people in technology. A large majority of them write programs, compile and run them. And a small minority write compilers. Language design is largely influenced by the latter group of people. Just as car mechanics always look at a car in a deeper way than the rest of us (they would use some technical jargons that others don't get), and astronomers looks at night sky in a different way than the rest of us, compiler writers have a deeper appreciation of programming languages.
So, writing a compiler, at least a toy compiler is a way to become a better programmer. There many resources available to write a basic compiler. And it might be easier than you think. For instance, grammar specification for python 2.7 is just a handful of pages (Though for python3 is a bit longer than that).
Programs get compiled to an architecture - sometimes to native architecture as in the case of C, and at other times to a virtual machine architecture (like in Java to JVM and in Python to python virtual machine). It is good to understand this architecture to have a better appreciation of the languages itself.
For instance, having an appreciation that instruction of integer multiplication and floating point multiplication are different would make one understand why int and float are two different data types. And understanding caches would help one understand why matrix multiplication done in a certain way is faster than when done in another way.
Another good exercise is to look at assembly code. For instance, you can use "gcc -S program.c" to generate machine assembly code, or javap to view the JVM assembly code for Java.
I cannot precisely defend why it makes one a good programmer but looking at an intermediate stage between source code and machine code certainly increases one's apprecation of what is going on.
Having an understanding of runtime environment helps you understand what is going on beneath the hood. For instance, understanding how garabage collection works in Java and why Global Interpretor Lock hurts python performance.
Reading history of anything equips one with a different take. Evolution of languages has been happening along with, and partly as a response to, increasing processing power and increasing penetration of computing in the society. Thus, while initial languages were closer the hardware, newer languages are more user friendly, and useful despite being inefficient because the hardware has become faster anyway. User friendliness of these languages have lowered the barriers to entry for programming.
Art is mostly not studied in isolation, but in association with the masters. Try to learn from other developers whose skills you admire. Having such a person as a mentor and getting feedback from them is invaluable. Another possibility is to read open source code and learn code programming styles from there.
For many developers, their coding cycle looks like the following. They are building some new functionality. Step by step they build it up. At each step along the way they test their code (mostly via print statements). Many times they encounter bugs which are not easy to resolve. They spend a lot of time find the logical error they have made. Finally, they are able to resolve all the known errors and check that the code correctly works for a all plausible use cases. At that part, they declare victory, delete extraneous print statements, perhaps do a bit of code clean up and send the code for review.
Just a hungry man cannot think of God, a developer who has not implemented the feature correctly cannot think of elegant code. The process of writing elegant code begins only after you have got the functionality out of the way. So, once you have implemented the feature, that is a good time to take a look if the everything can be done in a far better way. Often, I have found that only when you finish writing the code for a problem do you really understand the structure of the problem in the first place. And when you understand that, you can often do everything a bit differently, which is more elegant.
Very often I have seen developers trying to "future proof" their code. They develop extensive class hierarchies and needless abstractions justification for which is "in future when we need to do so and so, it will be very easy". In practice, the future turns out to be very different from what the developer imagined and new people reading the code waste time in understanding the abstractions for no benefit.
So, the code we write should be elegant for our current needs, not for enabling us to do better job in future. Only when it is very obvious that the requirements have moved in a way that existing abstractions are no more suitable, should we strive to change them.