Thursday, March 17, 2011

Be Assertive: Change your (programming) life using power of defensive thinking.

We use defensive programming extensively.
In the context of Radical Programming it boils down to three practices
  1. Make no assumptions about the input to your code. Don't trust it to be well behaving. Build a firewall of validation code around your public interfaces.
  2. Use assertions in code to check assumptions.
  3. Test the code thoroughly. Try every way to break your code.

Murphy's Law

Murphy's law states that if something can go wrong then eventually it will.
We can not predict all the ways in which our code will be called. Our assumptions may turn out to be wrong. Code branches you "know" will never be taken are executed. The conditions that must "obviously" be true are far from. Eventually in some case our code will fail miserably. Assertions are our friends.
Always test your code with debug build and lots of assertions.


Fail Often, Fail Fast, Fail Cheap.

Aim of the defensive programming is to discover as many bugs during development as possible. Ideally we should write as many assertions as possible. Initially there will be lots of failures and it will be difficult to get the code to pass unit tests or even the basic scenario running. However once all those issues are fixed they tend to not come back and what remains is a robust code.
Assertions are basically alternative way of comprehending your code. When your assertions fail in most cases they mean that your implementation is wrong, but sometimes your assumptions/assertions are wrong. Either way you have found a problem and end up in a better place.
Once you encounter the invalid data, you should immediately stop processing it to limit unexpected side effects. Failure in one place may not be visible until it creates mayhem in other component and results in data corruption. Best place to catch failures is before it starts cascades of failure. A stitch in time saves nine.

Three types of Checks

There are generally three types of checks.
1. Checks that help you during development and debugging
This is the bulk of assertions that go into debug builds. These are often preconditions and post conditions, loop invariant etc. The important 
2. Checks that must be part of release build
Parameter validation is one example. In case of public interfaces you must throw exceptions for invalid parameters so that caller knows what went wrong. If you use just debug asserts the assertion will not fail in release build and continue causing mayhem downstream. There are other genuine conditions where your code can not continue.
You have to make sure that asserts you use stops execution at the point of failure in release code.
3. Checks that validate the consistency of  state and data at various points.
It is important to check internal consistency periodically. For example if your internal data is a tree data structure then it is important to check that there are no cycles. This code is useful as assertions, in test code and even in released code as "feature".

Self document code with assertions.

Like unit tests the assertions are part of the active documentation. Every place in your code where you write a comment, think if there can  be a assertion instead.

Use of oracles

Oracles are alternative implementations that give the same results.
If using assertion is perfection then using oracles is divine.

Fulfill assertion's destiny by subjecting code to testing and static analysis.

Writing the assertions is only half the story. You must also exhaustively test the code.
Monkeying around
Unit tests and scenario tests are no doubt helpful, but it also helps to monkey around your code by actively giving it input that will break it.
Active Debugging
It always helps to instruct your debugger to break on assertion failure so you can actually watch your program fail.

 

Do not write clever code

Everyone knows that debugging is twice as hard as writing a program in the first place. So if you're as clever as you can be when you write it, how will you ever debug it?
-Brian Kerninghan
Often times the clever code fails because the requirements change and the underlying assumptions are no more valid. Cleverness is based on taking short cuts and to be frank it does "cleverly" use the context to achieve that. But when you add new functionality and refactor that context is gone. And you are left with the broken code.
The KISS principle viz. Keep It Simple & Stupid is still the best advice.

Ideas for coming up with great Assertions.

Books 

Toward Zero Defect Programming Writing Solid Code (Microsoft Programming Series)

Tools: 
PEX is a great tool from Microsoft Research

More Info


No comments:

Post a Comment