Stop writing bugs: Coping mechanisms and tools to prevent bugs.
You say your code has a bug when it does something you (or your end users) don’t want. The truth is that your code is always doing exactly what you told it to do. Your code acts the way it does because you wrote the code to do that—and that includes any bugs your code exhibits.
This is neither new nor surprising. But, to quote Dirty Harry Callaghan, “A man’s got to know his limitations,” and the first step in solving any problem is recognizing the problem.
Since we don’t write code to do what we don’t want, the definition of a bug is code you wrote thinking it did one thing but is, in fact, doing … something else. To put it another way: A bug is code you wrote but didn’t “fully understand all of its implications” (to put it politely).
So, after recognizing the problem, the second step is addressing the problem by building the necessary coping mechanisms: How can write code we understand better?
You’ve already been building those coping mechanisms: You do understand your code most of the time and, over time, you’ve gotten better at it (you used to write a lot more bugs in a lot simpler situations). The goal in this article, then, is not to provide some new technique because you’ve (probably) got them all. The goal is to make you more aware of how your current coping mechanisms contribute to preventing bugs so that you can get better at using them.
Let’s begin at the beginning: There’s a special class of bug where everything works from the developers’ point of view … but the end user isn’t happy with the result: a specification bug. Specification bugs occur in two scenarios—when the specifications can be understood in different ways and when the specifications are contradictory. In either case, there is an opportunity for the developer to implement the “wrong” choice (Developer: “I built what you said”; End user: “That isn’t what I meant”).
The solution here is ensure that the development team and the end users share a common understanding of what the specifications mean—that your specifications are valid. Having the developer and the end user review the specifications together is the best tool you have for achieving that (especially if the review involves using prototypes, which are the most powerful way to express the specifications in a way that’s meaningful to both developers and end users).
In terms of techniques, you’ve probably adopted some version of the single responsibility principle (SRP). In terms of bug reduction, SRP says that the less your code does, the more likely it is that you understand it: Shorter and simpler is better.
Let’s be more specific. You understand a method that fits on the screen better than a method where you have to scroll up or down to see all of it (and, for the record, switching to a bigger screen or a smaller font isn’t the right answer).
Of course, “shorter methods” means more methods so, if you follow SRP, your application is going to have more moving parts. “More moving parts” create the possibility for a new kind of bug: integration bugs, where components don’t play together as expected. While you’ve moved the problem around, you haven’t completely solved it (these are called “coping” mechanisms and not “solving” mechanisms for a reason).
Loosely coupling your components helps address integration bugs: In looking at any component, you shouldn’t have to know anything about any other component to fully understand the component you’re examining (in fact, the only thing two loosely coupled components should share is a message format). The definition of a microservice is probably something like “a loosely coupled SRP application.”
Very little of the code we write is pure boilerplate, but we do see the same kinds of problems cropping up again and again, which is where design patterns can be useful. Applying well-understood design patterns to your code has a couple of useful side effects in helping you understand your code.
First, if you pick the wrong design pattern, you’ll discover that you’re having to write a lot more code than you expected to make the pattern work (you picked the state pattern, for example, when the role pattern would better serve the problem). One of the signs that you understand your code is that the design pattern you apply really does make it easier to solve the problem.
Applying patterns also highlights what’s “special” about your current problem—an area where you’re more likely to misunderstand your code and introduce a bug.
And, of course, best practices and insightful commentary have grown up around all these patterns, all of which help you better understand your particular application of the pattern better.
You can further reduce the impact of having “more moving parts” just by giving your components names that describe what the component does. Don’t be afraid here of assigning long names in order to fully describe a component—any good IDE will be prompting you with the correct name, so you won’t ever type the whole thing, anyway.
Following well-known design patterns and creating components that fulfill roles in those patterns also help with naming: A component’s role in a pattern should drive the component’s name. If, for example, you’re implementing the factory method pattern, calling a method CustomerFactory may be all you need to understand that method (assuming that your CustomerFactory method is implementing SRP and doing nothing but living up to its role, of course).
Longer, more descriptive names also contribute to making your code readable, which also helps understanding—the simplest definition of “readable code” is that it’s code that “explains itself.”
When it comes to explaining things, you can learn from teachers. Any good teacher will tell you that you don’t really understand something until you can successfully explain it to someone else (even if that “someone else” is just a rubber duck that you keep around for that purpose). More importantly, the opposite is also true: If your teacher tells you something is hard to understand, it probably means the teacher doesn’t really understand it.
In other words, if you can’t explain your code in way that you and others find satisfying, then you probably don’t understand it.
The warning sign for this problem is that you start adding comments to explain what your code is doing (though comments that explain why your code exists are always worthwhile). Comments that explain the how of your code are an acknowledgement that, while you may understand your code right now, you’re pretty sure the next programmer won’t … and, in six months, you won’t either.
If comments are a sign that you don’t understand your code, then compiler warnings are a sign that the compiler doesn’t, either. When even the compiler thinks your code doesn’t make sense, it’s a pretty safe bet that your code won’t make sense to anyone else. Track those warnings down, understand why the code you wrote is triggering the warning, and replace that code with better code.
Writing readable code probably makes you think about coding standards. In terms of preventing bugs, the goal here is to pick standards and conventions that will either avoid or highlight misunderstandings. Indenting code, for example, isn’t about “making code pretty”—it’s about making sure that the visual layout of your code explains the structure of your solution and makes your code more readable.
In terms of coding conventions (e.g., naming variables), you should pick conventions that actively highlight errors that won’t be flagged anywhere else. Naming conventions that just flag data types (e.g., intCounter) don’t add any value because any good compiler or linter will already be flagging datatype issues for you (see: note about compiler warnings, above).
You prefer, instead, a coding convention that names variables based on the data they hold. You’d automatically wonder if you knew what was going on in a line of code that loads a variable named currentCustomer from a variable called currentSalesOrder, regardless of the underlying classes.
Adopting those practices also has the side effect of “standardizing code” across multiple applications. That standardization has value because it makes variations and unusual constructions stand out. Since these are the places where you’re mostly likely to misunderstand your own code, that’s a good thing.
As you can learn from teachers, you can learn from others who help people understand things. Any good technical writer, for example, will tell you that the easiest way to spot problems in something you’ve written is to put it in a drawer for a couple of days, then pull it out and reread it. If, on rereading your code you find it unreadable (i.e., it doesn’t explain itself), that’s a good sign you don’t understand what your code is doing.
Even better, as any technical writer will tell you, is to get someone else to read your code. (Which is why editors exist. Hi, Mandy! Hi, Mihail!) Tech writers call these “cold eyes” reviews. These reviews counter a kind of confirmation bias when you read your own work: You don’t see what the code says—instead, you see what you meant to write.
The key here is when you run into something that doesn’t make sense either to you or to some other reader, don’t try to either figure it out or explain it. Instead, rewrite your code so that it’s understandable as soon as you or the reviewer gets to it.
Perhaps your most powerful tool for avoiding creating bugs is to not write code at all. Instead, use popular third-party libraries wherever you can (like the various Telerik bundles). By reducing the amount of code you have to write, you both reduce the amount you need to understand and gain access to other developers’ understanding of that code.
Testing your code in isolation is only possible with a good mocking tool (like, for example, JustMock). And it’s going to be easier to test your code if you write it in a way that supports testing (though an effective mocking tool can make just about anything “testable”).
But, in a way, effective testing practices are a good idea even if you never run an automated test. Testable code follows the single responsibility principle and is loosely coupled with other components—both of which are techniques that support better understanding of your code. Having end users participate in writing tests at the integration level helps drive out “mis”-understandings in specifications. You could make the case that actually running the tests to prove that code has no bugs is just icing on the cake (but you should run the tests, anyway).
A zero bug count (i.e., where nothing is found in testing) would indicate a perfect understanding of your code. That’s probably not achievable—there’s a limit to human understanding, after all. But it’s a goal worth striving for.
Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter also writes courses and teaches for Learning Tree International.
Subscribe to be the first to get our expert-written articles and tutorials for developers!