In order to solve almost any problem, it is very important to understand three concepts: task, design and implementation. Well, maybe these words sometimes fit not well enough to represent what I mean here, but nonetheless, there are three important concepts to understand while trying to accomplish almost anything, especially if it is related to software development. But it also aplieth not only to software development.
Basically, task is what needeth to be done, design is how it is done in terms of external appearance, and implementation is how it is done internally.
Let us take an example. Suppose I have to write a calculator that can perform four basic arithmetic operations on floating point numbers. This is the task. Given the task, I need to design and implement it. Design issues would be: what user interface a program should have, on which platforms it should run on, how fast it should run on a particular piece of hardware, et cetera. Implementation issues depend on design ones. For example, if I want to design it so it will perform X operation N times per second (assumed that it is possible at all), I must think about how to achieve it–and this is implementation issue. If I want it to have command line interface, then I have to think about command line options. If I want it to have GUI, then I must think what GUI should look like, what toolkits to use, et cetera.
Note that I put execution speed requirement in the design, and it is not mistake because it affecteth the way how program can be used. If it is fast enough for manual use by human, it may be too slow to use it as a part of some script that performs some complicated scientific calculations. In this case, we shall say: it is not designed to be used in such way. Do not forget about premature optimisation, though: it is actually a type of mistake where thou triest to implement something that thou designed not, that is, thou triest to raise execution speed without any particular goal in mind, just “to be faster”. It is bad, very bad. Read The Art of Unix Programming by Eric S. Raymond.
One important thing about all this stuff is that it is flexible. Implementation is the most flexible part–it is usually entirely up to developer. Design is also flexible and different aspects of design can be mixed together or separated–for example, it is common Unix practice to write CLI program that doth the task and GUI that calleth CLI program as completely separated projects. As for task itself, it usually predefined, but can be often reformulated. For example, if thou hast to develop a calculator, thou shouldst think: needest thou really a calculator itself, or thou just needest a calculator in order to do something else? Is it possible to reformulate the task as “calculate something” instead of “write the calculator”? If it is, then the task is already done, since there are a lot of calculators already, and even more programs that can be used as calculators! So, the choice of task, careful design and smart implementation can really do wonders. On the design level, it may be a wise decision to go the Unix way so thou either needest not at all to implement GUI or at least canst do it as a completely separate task. On the implementation level, simply choosing Python instead of C can save thee weeks of work and give a lot of flexibility and portability for free.
Another important thing is that it is not that simple: task, design and implementation. In fact, every little bit of design is a separate task itself. For example, when thou decidest to have GUI (as a part of design process), thou automatically createst a new task: to design GUI, and whole philosophy applies to this task as well. So it all is a kind of a recursive process. Of course, on some level it becometh pointless to split thy work into further task / design / implementation parts, so recursion is not infinite ^_^ For example, while developing a calculator, thou decided (as a part of implementation process) to have a function that will divide two floating point values. So, “write function that divide two floating point values” is the new task. When designing this function, thou hast to think: how to name this function, what arguments should it accept, how it should handle errors (division by zero, for example), et cetera. On the implementation level, it is how it should work inside (that part that invisible from anywhere outside the function)–usually pretty simple part, but if thou wantest to write it in 8088 assembler without FPU, well, it might be not so simple ^_^
So, another important point is to clearly understand whether thou needest to think about design and implementation, or shouldst thou treat the whole task as atomic. It dependeth on a lot of things that thou shouldst decide for thyself. For example, if thou art writing simple shell command, there is usually no need to even design it–it is fine as long as it worketh. On the other hand, if it is a shell function to be put into ~/.bashrc, then it is maketh sense to at least design it such way so it will be easy to use in the future. Implementation details are usually unimportant in both cases, as long as thou indendest not to call it withing the long loop.
So, carefully think about task, carefully design the solution, carefully choose the implementation. Know that every sub-task is the task by itself. In other words: know what thou art trying to do in order to know what it should look like, think what it should look like in order to implement it properly, and implement as designed. At every point, there are sometimes unexpectedly simple solutions awaiting that were unnoticed simply because nobody ever thought about it this way.
There is a story about engineers that were trying to design a bulb for an electric lamp to be installed on a Moon research vehicle. It was a very hard research, and when they finally came up with something, they came to the chief engineer and asked, “How do you think, will it do?”. And he asked, “Hmm… Could you remind me please, why does the lamp need a bulb?” “Because otherwise, air will get in and the glower will burn out”, they replied. And then he asked, “Is there air on the Moon?” And then they understood that they were solving nonexistent problem. This was the story about carefully choosing the task. Note that it is not even about software development at all.
Design problems even need not an example to illustrate them. Creating GUI when nobody needeth it is one of the most frequent design errors. Why think I it is an error? Simply because it reduceth scriptability to zero. For example, if I have a CLI mp3 encoder (like lame), I can quickly write a script (even bat file in Windows will do!) that encodeth a thousand of files (and I did it many times, actually, maybe not for thousands of files, though)! If it is not CLI, but GUI, I have to live with what GUI giveth me, which is often not enough. And the most common design problem is not to design at all ^_^ This happeneth a lot–just take a look on some Internet URI and then read Cool URIs don’t change by Tim Berners-Lee, who is the WWW inventor, by the way. Some wise guys even think they need not URI at all–just make the whole thing in Flash! Great! No text operations (copy, paste, edit, etc.), no navigation except that was (if was) kindly provided by “designers”, no context menus… nothing except beautiful but stupid content! By the way, Flash designers (I mean those who designed Flash itself) might have given it a simple thought before making something that is intended to be used in the Web, but lacketh simple concept of URI. It is like creating a map where all objects are placed in the same location, with the attached list of “if you go there from here, then you will get to there” rules. Is not that nice? ^_- Those with weak imaginations, go and read XIV chapter of JRRT’s The Silmarillion (“Of Beleriand and its Realms”), it is very nice illustration to what I am saying.
Implementation errors are easiest to spot, so they tend to happen less frequently. But one common implementation error is to decide to design not parts of implementation, that is, to stop recursion too early. For example, if one thinketh about writing a function as about purely implementation thing, he may end up with defect implementation–for example, absent or inconvenient error handling. Another implementation error is to forget about what hath been designed–for example, trying to implement something that hath not even been designed, as with premature optimisation (the most common error of this kind).