The best way to reduce logic errors is to perform critical calculations two different ways.
By definition, this involves twice as much work as only solving the problem one way. The benefit, though, is that errors in logic become apparent before they can grow too large. I learned how devastating uncaught logic errors can be the hard way with my $86,000 mistake.
Let's look at some practical ways that you can incorporate automated double-checks into your applications:
- Test-driven development
- Different algorithms
- Third-party code
- Multiple programming languages
- Data validity checks
Types of Double Checks
Also known as TDD, this approach to development forces you to write tests to verify the correctness of your routines before you write the actual routines.
Entire books have been written on the topic of TDD. A simple web search can send you down rabbit holes for days. Before wandering through the woods investigating details, though, it helps to take a bird's-eye view of the whole forest.
Here's the high-level overview:
- Write tests and confirm they fail
- Write code until all the tests pass
Multiple Functions Using Different Algorithms
With this approach, you create two or more functions that take the same inputs and produce the same outputs. The difference between the functions lies with the implementation details.
For example, if your critical logic involves a sorting routine, you could use a bubble sort in one case and a quick sort in the other. You would then compare the results of the two functions to ensure they always matched. If the outcomes don't match, you raise a runtime error so that you can deal with it.
Remember, a big part of creating easy-to-maintain code involves finding ways to promote errors from the bottom of the following list to the top:
- Syntax errors
- Compile errors
- Misunderstood requirements (before you start writing code)
- Automated test errors (i.e., failing tests)
- Runtime errors
- Misunderstood requirements (after you've written the code)
- Logic errors
Turning a logic error into a runtime error is a big win.
It's not always easy to write two different algorithms to solve the same problem.
If a single developer writes both algorithms, they are more likely to make the same kind of mistake in both places than if two different developers create the algorithms. If you are a solo developer, this puts you into a bind. You have no choice but to write both algorithms.
Depending on the nature of the problem, you may be able to find an existing solution to the problem on the internet.
Before blindly copying and pasting that solution into your program, I would challenge you to develop your own solution before reviewing the code you found online. Once you've done that, you can compare the results of your calculations with the code you found online.
If both pieces of code produce the same outputs, then hey, great minds think alike! If there are differences, then you can compare the two approaches and see where the problem lies (they might both be wrong!).
Multiple programming languages
Different languages have different ways of doing things.
Python articles and tutorials regularly refer to a Pythonic way of designing code. This often involves thinking about a problem from a completely different angle than what you might be used to.
The constraints and differences among programming languages will force you to vary the approach you use to solve a problem.
- Procedural: VBA, C#
- Functional: F#, Haskell, LISP
- Declarative: SQL, XAML
- Concurrent: Go
- Scripting: PowerShell, AutoHotkey
Data validity checks
Referential integrity alone is not enough to ensure that your data is valid.
Some business logic rules are difficult or impossible to enforce within your database, even with the use of CHECK CONSTRAINTs or triggers in SQL Server. For those situations, I create a series of SELECT queries that will identify invalid data.
Here are some example descriptions of data validity checks that I use in our real estate tax assessment (CAMA) software, Assessor2k:
- Properties with approved homestead application but no dwelling.
- Properties whose current ownership record does not reflect the most recent deed.
- Properties with approved active homestead application but applicants who are not part of current ownership.
- Clients with DEC'D in their name but no Date of Death listed.
I'm not suggesting that you write every function in your program twice.
That would be insane.
Instead, apply the 80/20 rule. Identify the critical functions that would cause the greatest damage if you got them wrong. Then–and only then–use the techniques in this article to ensure that if your critical functions break, then someone will notice.