focus for this week: Why don't birds fly backwards ?
>1.1 mio views (popular pages, total: 2,030)
Open Advice/The Art of Problem Solving
The Art of Problem Solving
- by Thiago Macieira
- in: Open Advice
This text is available under the CC-BY-SA license. (see also: Open Advice/Info)
Contents |
Problems are a routine we are faced with almost every day of our lives and solving them is so recurrent we often do not realize we are doing it. They may be situations as simple as figuring out the best path to get to a destination or how to set the items in the fridge so they fit. Only when we fail to solve them immediately do we take notice, since we have to stop and think about them. The professional life is no different and solving professional problems becomes part of the job description.
Problem solving was the topic of my inaugural class when I started my engineering degree. In that overcrowded amphitheatre last century, our professor explained to roughly 700 freshmen how engineers are problem solvers and our professional lives would be moving from one problem to be solved to another. Some problems would be small and we would solve them in no time; some others would be so big we would need to have a project setting and a team to crack, most would fall in-between. He then proceeded to give examples on how the mentality of “problem solver” helped him in his professional and personal life, including one unexpected live example when the projector failed on us.
The ability to solve problems is a skill we can hone with practice and some ground work. Practice is something one must acquire only through experience, by trial and failure, therefore it is not something that a book could teach. Getting started in solving problems, however, is something one can learn. If experience is the toolbox we carry when facing new issues, the techniques of problem solving are the instructions on how to use the tools in the toolbox.
Phrasing the question correctly
The question we are trying to answer is the direction we are going to go when trying to solve the problem. Ask the wrong question and the answers may be irrelevant, invalid or just plainly wrong. Consequently, asking the correct question is paramount. Moreover, asking the correct question correctly is important, since it provides clues as to what we are seeking.
The most useless problem statement that one can face is “it doesn’t work”, yet we seem to get it far too often. It is a true statement, as evidently something is off. Nevertheless, the phrasing does not provide any clue as to where to start looking for answers.
Bug-tracking systems often request that the bug reporter describe the actions taken that led up to the problem being seen, the description of what happened (that is, the symptom) and a description of what was expected to happen. The comparison between the symptom and the expected behavior is a good source for the question to be asked: why did this happen, why did this other behavior not happen? While this is not the only way for creating the question, applying this technique to problems may certainly help.
Phrasing the problem and the question correctly, in all its details, is also a way to further describe the problem statement. First, we must realize that the problem very likely does not lie where we are expecting it to be – if it did, we would have probably solved the problem by now. Explaining all the details of the problem at hand provides the help-givers with more information to work with. In addition, even if counter-intuitively, the act of describing the problem in its entirety often leads to finding of the solution, so much so that many development groups require “stuck” developers to perform this task, either by discussing it with a colleague or talking to a “naïve” entity, like a rubber duck or Mr. Potato-Head.
In addition, one must return to the question every now and then, so as to not lose sight of what the goal is. While executing activities to solve the problem, care must be taken not to concentrate exclusively on a particular piece of the problem, forgetting the overall objective. For the same reason, it is necessary to reexamine the initial question when a possible solution is found, to ensure it does solve the entire problem. In turn, this also shows the necessity of asking the right question, stating the complete problem: without the full question, the solution may be equally incomplete.
Divide et conquera
Experience in helping others trying to solve their problems online has shown me that in general people treat their issues as monolithic, indivisible stumbling blocks that must be dealt with as a whole. As such, a large problem poses a very difficult question to be answered in its entirety.
In truth, the vast majority of those issues can be further broken down into smaller problems, each of which are easier to deal with and determine if they are the root cause of the problem, not to mention the possibility of there being multiple sources for the symptom experienced. Repeating this operation just a couple of times will yield much smaller problems to tackle and, therefore, quicker solutions. However, the more divisions we are forced to make, the more we are required to know about the operating internals of the system at hand. In reality, the problem solver will only break down as far as his knowledge of the subject will permit and then work on the issue from there.
For software development, the subsystems being used are often good hints at where to break up the problem. For example, if the problem involves a TCP/IP transmission of data, two possible divisions are the sender and the receiver: it is of no use to look for the problem on the receiver’s end if the sender is not transmitting the data properly. Similarly, a graphical application that is not showing the data that it is fetching from a database has a clear division: it would be a good idea to verify that the database access works before investigating why it is not displayed properly. Alternatively, one could feed dummy data to the display functions and then verify that said data does get displayed properly.
Even when the groupings are not clear, dividing the problem can still help shed light on the issue. In fact, almost every division is helpful, as it reduces the amount of code to be inspected, and with it the complexity to be dealt with. At an extreme, simply dividing the code in two and searching for the problem in one half may be of use. This technique, called bisecting, is recommended if the divisions created from the subsystems and interfaces have not yet revealed a solution.
The end-product of a sequence of proper divisions is a small, self-contained example showing the problem. At this stage, one of three options is usually right:
- the problem can be identified and located;
- the code is actually correct and the expectations were wrong;
- or a bug was found on the lower layer of code.
An advantage of the process is that it also produces a test-case to be sent in a bug report, should a bug turn out to be the cause.
Boundary conditions
An issue similar to dividing the problem is that of the boundary conditions. In mathematics and physics, boundary conditions are the set of values for the variables that determine the region of validity of the equations being solved. For software, boundary conditions are the set of conditions that must be met for the code to perform properly. Usually, the boundary conditions are far from simple: unlike mathematics and physics, the variables in software systems are far too many, which means that the boundary conditions for them are equally many.
In software systems, the boundary conditions are often referred to as “preconditions”, which are conditions that must be met before a certain action is allowed. Verifying that the preconditions have been met is good exercise in the searching for an answer, for a violation of the preconditions is definitely a problem that needs solving – even if it is not the root cause of the original problem. Examples of preconditions can be as simple as the fact that a pointer must be valid before it can be dereferenced or that an object must not have been disposed of before it can be used. Complex preconditions are very likely to be documented for the software being used.
Another interesting group of boundary conditions is characterized, interestingly, by what is not permitted: the undefined behavior. This type of boundary conditions is very common when dealing with specifications, which try to be very explicit in how software should behave. A good example of this are the compilers and language definitions. Strictly speaking, dereferencing a null pointer is an undefined behavior: the most common consequence is a processor exception being trapped and the program terminating, but other behaviors are permitted too, including working perfectly.
The right tool for the right job
If engineers are problem-solvers, the engineer’s motto is “use the right tool for the right job”. It may seem obvious, as no one is expected to use a hammer to solve an electronic problem. Nonetheless, cases of using the wrong tool are quite common, often due to ignorance of the existence of a better tool.
Some of these tools are the bread-and-butter of software development, like the compiler and the debugger. Inability to use these tools are unforgivable: the professional who finds himself in an environment with new or unknown tools, such as when switching positions or jobs, must dedicate some time to learning them, becoming familiar with their functionalities and limitations. For example, if a program crashes, being able to determine the location of the crash as well as variables being accessed in that section of the code may help determine the root cause and thus point to the solution.
Some other tools are more advanced, belong to a niche, are not very widely known, or are available only under cost or conditions which cannot be met by the engineer. Yet they can be incredibly useful in helping elucidate problems. Such tools may be static code checker tools, thread checkers, memory debuggers, hardware event loggers, etc. For instance, development hardware often contains a way to control it via a special interface like JTAG or dump all instructions executed and processor state, but this requires having special hardware and tools, which are not readily available and usually cost more than volume, consumer devices. A different example is the valgrind suite of tools, which include thread checkers and memory debuggers and is readily available for free, but are part of the advanced, niche tools and are not taught at schools.
Knowing the contents of one’s toolbox is a powerful knowledge. Using a specialized tool to search for a problem will likely yield a result quicker, be it positive, confirming the problem, or negative, which in turn leads the search elsewhere. Moreover, it is important to know how to use these tools, which justifies spending time reading the documentation, in training or simply experimenting with them with known problems to understand how to proceed.
Conclusion
Solving problems is an art available to all. Like other arts, some people may have such a skill that it may seem that they were born with the ability. But in reality, with enough experience and practice, solving problems becomes an unconscious activity.
When faced with a problem that is not easy to solve, one should sit back and take a clear look at the entirety of the problem.
What is the problem we have?
Can we phrase the question that we need an answer for?
Once we know what we are looking for, we can start searching for where it may be located.
Can we break it down into smaller, more manageable pieces?
What are the best tools to be used for each piece?
Have we verified that we are using the functionalities and services correctly?
After solving many problems, we start to see patterns. It will become easier to detect subtle hints from the symptoms and direct the searching towards the actual problem. An experienced problem-solver may not even realize this action is taking place. That is an indication that the experience and behavior has set in so well that no conscious effort is required to access those skills.
Yet there are always some problems in life that will be hard to solve, ranging from professional, existential, philosophical or for pure curiosity. Then again, it is the challenge that drives us, the need to understand more. Life would be pretty tedious otherwise.
info about the author
Thiago Macieira holds a double degree in Engineering and an MBA, but his involvement in Open Source predates those, getting close to 15 years now. An active participant in the KDE, Qt and MeeGo communities, he’s been a software engineer and product manager for Qt, giving presentations and listening to people. These days, Thiago lives in Oslo, Norway and when he’s not working on Qt, he tries (with limited success) to improve his skills at StarCraft 2.