I believe that the best way to get better programs is to teach programmers how to think better. Thinking is not the ability to manipulate language; it’s the ability to manipulate concepts. Computer science should be about concepts, not languages. — Leslie Lamport
Most developers love to write code. Many of us code in our free time. This is where we’re most comfortable. It’s what we’ve been trained to do. When it comes to our jobs, it’s easy for us to see ourselves as coders. It’s easy to believe that our job is to translate requirements into code.
Our job is not to write code. Our job is to solve problems.
Even if you’re not a professional, it’s still not enough for you to just code — not if you want to be good. It’s not enough to know how to work with standard algorithms. It’s not enough to memorize languages, design patterns, and best practices.
Good programmers know how to problem solve.
To move from coding to problem-solving, we need to change how we think and how we work. This begins with a change in perspective. Then, it involves understanding how to problem solve.
The first step in understanding how to solve problems is understanding problems themselves.
Most problems are more complicated than people realize. They have many facets. Like chopping off the head of a Hydra, if you get rid of one problem, many others can come to take its place. Problems have perspectives. A problem for one person may not be a problem for another. It may be a benefit!
Here’s a “simple” example. Consider a small town with one market. Customers of the market are complaining because the price of apples has gone up. Apples are a staple of the diet in this town. The market owner is refusing to lower the price of the apples — even though apples in the market of a neighboring town cost less.
What is the problem here?
On the surface level, you could say the problem is that the market is charging too much for its apples. But this simple analysis misses important things.
To start with, there are several perspectives on what the problem is:
Knowing the perspectives of the different groups affected by the problem lets us know what outcomes they want. While this is helpful, it also creates a new set of problems:
Then, there’s our perspective as problem solvers. What biases are we bringing to the situation? How much do our own needs, and our tendency to not see our contributions impact our ability to fully see the problem? How well do we really understand the problem? In a fast-paced world, where quick answers are often rewarded, it’s easy to take the stated problem at face value and rush to solutions.
However, if you can’t think of at least three things that might be wrong with your understanding of the problem — you probably haven’t spent enough time with it. As you can see from the simple example listed above, lots of things can be missing from a problem definition.
It’s not just the problem definition that can be rushed. If you can’t think of at least three solutions to the problem — you probably haven’t spent enough time brainstorming.
Consider the problem of maintaining a specific level of water in a tub. What are our options? Many people will say: “That’s simple! Fill the tub up with water to the level you need, then plug it with a stopper.” Is this the best option? Maybe. It depends. Why do we need to maintain this level of water in the tub, to begin with? What problem is that solution designed to solve?
Let’s assume, for now, that we do need to maintain the water level in the tub. Is plugging the tub the best solution? It’s not the only solution. Here is another one:
Is this a better solution? It depends on the problem that maintaining the level of water in the tub was designed to solve.
In addition to perspectives, problems have context. If we just look at the problem on a surface level, we may be missing important information. Let’s return to our problem of the market with the increased price of apples. What happens to your perception of the problem when you start asking a few background questions:
The answers to these, and many other questions, could change how we view the problem dramatically.
Let’s say that we came up with a “solution” to our apple problem. We determined that the market needed to lower the cost of apples. This satisfied the customers. However, now we have new problems.
The market had to absorb the increased cost of apples. Therefore, it could no longer afford to sell oranges. This didn’t have a significant impact on the town’s population. They didn’t like oranges much. However, it did impact the market’s relationship with their supplier. The supplier, now, is threatening to drop the market because it’s no longer meeting its minimum fruit order. Furthermore, if the supplier drops this market, it will drop all markets in the surrounding area, because there will no longer be enough markets for the supplier to continue to make a profit.
The town cafe, which was selling apples on the cheap to draw in customers, no longer has that draw. They are experiencing a downturn in business, for which they are not happy. So… now our poor market owner has all kinds of people furious at him!
As we’ve discussed, every problem has a context. It’s important to keep in mind that the problem solver is also a part of this context. Your relationship to the problem has an important impact on your ability to solve it well.
But wait, you may be saying, I solve problems all the time and I don’t deal with all this craziness! Enter some of the most difficult factors in problem-solving: Time, Linearity, and Systems.
Because of feedback delays within complex systems, by the time a problem becomes apparent, it may be unnecessarily difficult to solve.
— Donella Meadows
Time is one of the most complicated factors in understanding the impact of an action. When there is a time gap between an action and a consequence, humans have difficulty making causal connections. This fact is eagerly exploited by credit card companies who allow you to buy now and pay later.
The issue of time is compounded by the fact that, as engineers, we work with — and within — systems. In addition to this, we tend to think linearly in simple patterns of cause and effect. However, systems are more complex. They often have multiple things happening at the same time. Systems are jumbles of connections in many directions, simultaneously. A symptom in one place could be the result of several causal factors. Changing one of those factors can have unpredictable impacts.
If you have a situation where you seem to be stuck with long-term problems — you may be dealing with feedback delays or patching symptoms. In these situations, it can be very difficult to see the systemic connections and relationships that are driving circumstances.
Problems are often more complicated than we see — especially when time and complex systems are involved. It is easy to believe that a problem is solved, only to realize with time or new information that it’s not — or that other problems have come to take its place.
At the beginning of this article, I said that just knowing how to code wasn’t enough. You had to know how to problem solve. So far, all the examples we’ve looked at haven’t involved code. Surely, when we’re dealing with code, things are different… right? Nope.
Let’s look at an example of a simple, common interview task:
Write a function that counts the duplicates in an unsorted array of numbers.
That seems pretty straightforward — at first glance. But, we’re a bit wiser now than we were before. What could we be missing?
What other questions would you ask?
This is another good example of how a seemingly simple problem statement is more complex than it appears. It is also, incidentally, a good example of how decay and complexity can enter a system. There are lots of algorithms just like this one in most applications. These functions start off simple — often due to a simple understanding of the problem they’re trying to solve. Then, they get refactored for various reasons. Those reasons are rarely documented. Sometimes, they’re validated with unit tests — if a unit test is appropriate. The impact of this multiplies over time, as the refactoring done for one scenario impacts the refactoring done for a previous scenario. With each refactoring, functionality can be subtly impacted, and information lost — thus creating new problems that may only be discovered over time.
So, what’s an engineer to do? Problem-solving is… a complicated problem! There are a lot of possible answers. Here are 10 things to keep in mind: