Easy Debug
When you code, you'll likely spend more time fixing mistakes than writing the initial code. This is a well-known fact.
Here's the point: knowing that coding is mostly debugging, what do you do about it?
One way to approach it is to focus on writing fewer bugs in the first place. That's what this book is about.
But here's another angle: how can you write code that's easy to debug?
The Lifecycle of a Bug
Debugging is a 4-step process:
- Detect - find the problem.
- Diagnose - figure out what's causing it.
- Fix - change the code to fix the issue.
- Test - make sure the fix didn't create new problems.
The hardest part is diagnosing the problem. Usually, you get a description of the symptom, but not the cause. You need to figure out what led to the symptom and what went wrong.
Computers are deterministic, so if you reproduce the situation exactly, you'll get the same result. If you don't, you didn't do it right.
Imagine having a time machine and being able to see exactly what caused the problem. That would make debugging easy! But we can't, so we need to fake it. We get the code back to the situation that causes the problem and break into the debugger just before it goes wrong.
The key is knowing when to break in. Sometimes, the problem and symptom are the same thing. But often, there's a gap between them. In those cases, it's like a magic trick to figure out when to break in and get the right information.
When a bug is hard to find, it's like trying to identify the starting point of an avalanche. You need to work backward step by step to understand what went wrong.
Usually, it's not one giant mistake that causes the problem. Instead, a series of small mistakes lead up to the final symptom. To fix the bug, you need to identify each of these mistakes and work backward to the original cause.
It's easy to get distracted by the symptoms and try to fix them without understanding the underlying cause. But this only removes the surface-level problem, not the root cause. It's like removing a pebble from an avalanche, but leaving the rest of the snowfield intact.
The key to effective debugging is taking the time to work backward and identify each step that led to the symptom. This allows you to fix the root cause, rather than just patching the symptom. By making it easier to work backward, you can resist the temptation to fix symptoms and instead fix the underlying problem.
Minimizing State
Debugging is easier when you follow these rules:
- Reduce the length of the causal chain: Symptoms with a single cause are easier to fix than those with a long chain of causes.
- Make it easier to hop backward in time: If you can reproduce the state that led to the cause of each symptom, you can explore the causal chain.
- Push symptoms closer to causes: If the cause is nearby or recent, discovering the connection is easier.
If you eliminate unnecessary state and use pure functions, debugging becomes easier because you can reproduce the problem more easily. However, in many cases, code needs to model real-world objects with virtual analogs, which means it can't be entirely stateless.
Still, try to reduce state wherever possible. State makes debugging harder, and coding is all about debugging.
Unavoidable State
When state is unavoidable, it can make diagnosing problems more complicated.
The problem is likely related to the state of the interacting objects, probably inside the character. To diagnose the problem, you need to reproduce that state.
If the bug shows up 100% of the time in a unit test, you're lucky! If you don't have a unit test, you can manually reproduce the problem and detect it.
In the old days, I'd insert code to handle this. Now, I can set the next line of execution in the debugger to jump backward in the code and identify the problem. Every little bit of state eliminated can make diagnosis easier.
Unavoidable Delay
Detecting symptoms isn't always easy. Sometimes problems aren't immediately apparent.
If your code relies on state, capturing that state can make debugging easier. You can create an executable log file by capturing inputs and allowing them to be replayed. This is especially important for complex systems where debugging is crucial.