Writing Solid Code

I often read job descriptions, and they usually say something like “Must have a strong understanding of the principles of SOLID software development.” I feel like I understand SOLID, but on the spot I could NOT recite and provide an example of each. I mean you, Liskov Substitution Principle. I wanted to improve, so I began searching Amazon for a good SOLID book. I happened to run across “Writing Solid Code” by Steve Maguire. Though I was initially fooled by the title, it’s not a book about SOLID principles. This is a book about improving software quality through good programming practices. Writing software was harder 25 years ago, and I figured that he had some strong, timeless lessons. So, through the magic of Prime shipping, I had the book in my hands the next day.

Writing Solid Code

Validating Assumptions

Maguire strongly advocates flooding your codebase with Assert statements to validate assumptions that should never be false. With also these Asserts, he reasons, we should be able to capture more defects in QA.

At that time, an Assert took the form of a C macro that ran based on a preprocessor flag (such as #DEBUG), and was removed in production code. Due to the speed of processors at the time, the effect of these Asserts would be a 50% slowdown while testing, with no effect in production. Here is an example from the book:

/* strdup -- allocate a duplicate of a string. */
char *strdup(char *str)
{
    char *strNew;
    ASSERT(str != null);
    .
    .
    .
}

Maguire goes to great lengths to differentiate between errors and illegal conditions. Assertions are for illegal conditions, not errors. A condition such as bad user data is an error that most definitely will show up in the production environment, so it must be detected and handled.

Debug.Assert

Fast forward a few years, and we have Debug.Assert. Your code can validate these assumptions in debug mode, and that code will not run in production mode.

Debug.Assert("something that cannot be false")

However, with processors running much faster, I think the overhead of these assertions would be negligible. It would be an interesting exercise to prove that. But, the same caveat applies — your code still needs to use other means to defend against assumptions that “could be” false, such as bad user input.

Exception Handling

A different way to validate assumptions is by using exceptions.

if(this.someArray == null) throw new InvalidOpertionException("this cannot happen")

You can also use exceptions to validate user input, too.

if(string.IsNullOrWhitespace(viewModel.firstName)) throw new ArgumentException("this is bad data")

Debug.Assert vs Exception Handling

So, given those two choices, when should a programmer use Assertions, and when should he or she use Exceptions? I read through several well-reasoned responses on StackOverflow, and I think Eric Lippert captured this well:

  • It should ALWAYS be possible to produce a test case which exercises a given throw statement. If it is not possible to produce such a test case then you have a code path in your program which never executes, and it should be removed as dead code.
  • It should NEVER be possible to produce a test case which causes an assertion to fire. If an assertion fires, either the code is wrong or the assertion is wrong; either way, something needs to change in the code.

So, he is talking about visibility here. For a public method that is accessible to a unit test, you should throw. If the public method calls a private method, it might Assert.

public void Log(string message) {
    if(string.IsNullOrWhitespace(message)  {
        throw new ArgumentNullException(nameof(message));
    }
    WriteToLogFile(message);
}
private void WriteToLog(string message) {
    Debug.Assert(string.IsNullOrWhitespace(message) == false, "message cannot be null or whitespace");
}

In this example, I could write a unit test to create my exception, but I could not write a unit test that violates my Assertion.

Data Contracts

More recently, Microsoft has provided us with Data Contracts. These contracts are evaluated during development, and optionally evaluated in production. So, here’s what they look like. For validating the first case, an assumption that “cannot be” false, we can do this:

Contract.Assert(result != null, "this cannot happen");

For validating the second case, an assumption that “could be” false, we can do this:

Contract.Requires(parameter != null, "parameter");

This is a vast improvement for a few reasons. First, a single Contract assertion is more readable and less error prone than a block of exception logic. Second, and without describing every feature, Contracts are more flexible than assertions. Third, Contracts are available for tooling. So, an IDE can expose assertions in tooltips, or in a documentation generator. Finally, and probably most relevant to code quality, because Contracts are visible to the tools, the IDE can provide better static analysis. That means you will be alerted to potential quality issue at compile-time, rather than run-time.

Conclusion

To summarize, covering your code with assertions can be one tool to improve code quality. Know the difference between Asserts and Exceptions, and use Contracts if it fits your environment. I’m thankful to Mr. Maguire for reminding me of this advice. His book contains many more timeless lessons, and as programmers improving our craft, we can learn by looking back as well as forward. Right now I’m recalling his advice to use a Linter, which I didn’t even know existed before JSLint!