Chapter 5: Candy-Machine Interfaces

  • make it hard to ignore important details by making them explicit in the design;
  • create interface that are easy to use and understand;
  • return values must not carry error codes;
  • look for and eliminate flaws in your interfaces so others don’t follow your mistakes;
  • use single responsibility principle for methods, don’t write multi-purpose methods;
  • define explicit method arguments. Consider how others will call your method. Think about the method’s purpose and if it makes sense to call the method with an odd value. If it makes sense, the odd value should be treated differently in the method, otherwise don’t stress about it, just assert it;
  • write methods which cannot fail if they are given valid inputs;
  • examine your interfaces from the caller’s point of view;
  • make the code intelligible at the point of call by using proper arguments. Avoid using boolean and magic number arguments, this is usually a smell of the method having two purposes instead of one and usually ends up in splitting the method in two others;
  • comment code to emphasize a potential hazard if the hazard cannot be removed. Comment odd cases and what to be careful around, but again, only if the hazard cannot be removed. Comments should not be an excuse to write poor quality code;
  • the devil is in the details. Write everything as clear and obvious as possible.

Chapter 4: Step Through Your Code

  • the best place to look for newly introduced logic bugs is in the new or changed code;
  • it’s best to test new/changed code by setting a breakpoint in the new/changed code and stepping into it with the debugger. This way you can be sure the new code is actually executed (not skipped by some weird business logic for example);
  • every code path should be checked using the debugger and stepping into the code. Usually, the error handling cases have more bugs because that code path is rarely used;
  • a fast way to force an error and test the error handling cases is to step into the code with the debugger and forcing/setting the desired value to some variable to make sure the code will follow the path you desire to test. Even though it’s common sense how to do it, most people don’t (unless they hunt a bug);
  • danger zone: ||, &&, ??, ? operators. Why? Because the debugger will evaluate two conditions in a single step. Use debugger (immediate window) to analyze both conditions to check if they are as expected. Due to the short-circuit evaluation of the || operator for example, if the first condition is true, the second condition is not even evaluated. If in some cases, the code relies on the second condition to do something, and if the second condition was never tested, a bug might come up. So always check both code paths.

Chapter 3: Fortify Your Subsystems

  • look at your subsystems and ask yourself how likely are other programmers to miss-use them. Add assertions and validation checks to catch hard to spot and common bugs;
  • remove random behavior to force bugs to be reproducible;
  • destroy garbage data/objects in order for them not to be misused;
  • look for rare behaviors in the program and try to make them happen more often (even in the debug versions of the code with directives). A rare behavior might have a bug which is not easily noticed because the behavior itself is rare. This kind of bug can be very difficult to track down. If one were to make this behavior execute more often, there is a good chance that the bug will be discovered eventually;
  • all implementations should be well thought keeping in mind whether a possibility either generates or helps finding bugs;
  • nothing should be arbitrary. Analyze all options for as long as you need before taking a decision;
  • for each design you consider, ask yourself how can you thoroughly validate it. If the implementation is difficult or even impossible to test, seriously consider a different design even if that means trading speed/size for the ability to test the system;
  • the debug version is not shipped, you can do whatever tests you want in the debug version. Even if it’s slower, if there is a chance to catch a bug before the code reaches production, it’s a win-win situation for everybody;
  • if debug code is about to be tested by somebody else, warn them about the code being loaded with internal debug checks which affect performance;
  • don’t apply ship version constraints to the debug version. Trade size and speed for error detection.

Chapter 2: Assert Yourself

  • assertions (not to be confused with Unit Test Assertions) are neat way to validate the data your code works with in order to determine potential corrupt data as close to the time of the corruption itself. Read more about Assertions vs UnitTest Assertions;
  • preprocessor directives (#if DEBUG) are old school and I wouldn’t use them unless a critical situation arises (and even then I would probably use it temporarily). I find them to be messy, make code harder to read (not clean code);
  • don’t make/rely on assumptions (assumption is the mother of all fuck-ups). Remove them or test them.
  • defensive programming usually hides bugs and should be avoided. However, if really needed for some reason, make sure to test even the most defensive programmed part you wrote.
  • execute debug code in addition to, not instead of ship (aka release) code/configuration;
  • keep an eye of opportunities to validate the results a method returns. Bottlenecks are pretty good methods to look into;
  • be sure to use a different algorithm (not another implementation of the same algorithm) to validate the results to increase the chance of finding bugs in the actual algorithms;
  • it’s not a tester’s job to test your code, this is your job;
  • when noticing something risky in the code, try to think what can you do to catch a potential bug in that specific area, automatically, at the most early stage. This exercise will translate in finding many ways to make the code more robust and therefore safer;
  • when you start using assertions, most of the times the bug counts will climb dramatically. This can alarm people and make people uncomfortable if they are not aware of the assertions (even if it’s for the greater good). So always warn your colleagues that the number of bugs could be increasing after using assertions;
  • assertions should be kept forever and not stripped out once the code has been released. These assertions will help in the future when new features will be added in the next version;
  • use assertions to catch illegal conditions which should never arise. Do not confuse these conditions with error conditions which must be handled in the final product.

Chapter 1: A Hypothetical Compiler

  • enable all optional compiler warnings to see from the beginning if the code starts to slip;
  • enable FxCop to avoid compiler-legal code which is likely to be wrong (ex: not using the disposable pattern on an object which implements the interface);
  • always compile and run unit tests before checking in even if the change was trivial (even if it was just a refactoring with no new written code);
  • find bugs as early and easily as possible.

Conclusion: you want to catch the bugs automatically, at the earliest possible stage without relying on someone else’s skills.