Continuing my chapter-by-chapter review of A Philosophy of Software Design By John Ousterhout, today I’m going to review Chapters 10 and 11.
Brief Interlude
Nine chapters in to this review of A Philosophy of Software Design, I think it’s worth taking a quick moment to talk about why I’m doing it. I’ve been wanting to put together something of a milestone of sorts, a checkpoint in my career that I can show where I am and eventually look back on to see where I’ve been. Trying to write out what I know (or think I know) about software development is a large and nebulous task. As a writer, I’m probably not up to the task. However, being able to follow along with an existing book, well-written by a well-respected author gives me a chance to define my current state as a delta instead of trying to create a snapshot from scratch.
There are many places where I agree with John and several places where I don’t agree. It’s not that I think I’m better at this gig than he is. Quite the reverse, in fact. I’m trying to quantify myself by comparing myself to a known and substantial value.
It’s worth mentioning that there are a few other reviews of this book throughout the blogosphere. One that I found particularly interesting, because the author claims he worked with John Ousterhout while writing it, is this one by James Koppel. He had some disagreements with the book, but also found real value in it. If there’s one thing that we’ve learned about programming since the inception of the discipline it’s this: there’s no magic bullet. There’s not one right way to go about it. What we can do is learn about how other people have found success, and try to incorporate those lessons into our own way of thinking.
One other thing I will say is this: I don’t bother to criticize things which I don’t care about. It’s not a productive way for me to spend my time. If I didn’t believe that there was a heck of a lot of value in this book, I wouldn’t waste my time talking about it at all. If you haven’t yet, do yourself a favor and pick up a copy. It’s not expensive as far as technical books go and it’s a great addition to any bookshelf.
Chapter 10: Define Errors Out Of Existence
I use the term exception to refer to any uncommon condition that alters the normal flow of control in a program
Take note: this is very different from the formal Exception
objects and throw
/try
/catch
mechanism employed by many modern languages. Understanding all the many things that represent exceptional code and the sheer scope of handling exceptional conditions is key to understanding what John talks about in this chapter. It’s more than you expect! Even the C-style if (!result){ ... }
kind of error checking with flags and status values passing around is considered “exceptional” here. Basically there’s the happy path where the software does exactly what it is supposed to do, and then there’s every other possible code flow. If you think about it this way, some programs are nearly 90% exceptional!
Now that we know what the issue is, what’s the downside? Well, besides the standard try
/catch
/finally
block being phenominally ugly, these things make code flow much harder to understand and thus increase complexity.
So what do we do about it?
Define errors out of existence
Basically, be much more tolerant of what you accept. Coalesce null values to some default value. Automatically adjust out-of-range indexes to be in range. Use the Null Object Pattern.
Acquaintance and CastIron both do a lot of this, I try not to throw any Exceptions or return error flags at all unless I’m in a situation where a sane fall-back strategy can’t be determined, or where the user of these libraries has made a preventable error that should be corrected before going into a production environment. Once the user has communicated their intent, the system should satisfy it. Steps should be taken, through careful interface design, to try and ensure that users cannot communicate an invalid intent.
Exception masking
Catch the exception as close as possible to the source and handle it. You can convert an exception into an error log entry and a false
return value, or retry until the error resolves itself.
I find myself often using exception masking to work around design deficiencies in the .NET standard library. Here’s a snippet that pops up much more often than I would like:
var intValue = int.TryParse(intString ?? "0", out int x) ? x : 0;
What we really want in most cases is to just get a damned integer from a string, even if the string isn’t in a perfect integer format. Getting a C# int
from a database column of type INT NULL
is another case that is fraught with danger but pops up all the time. Sometimes what you want is just to “get me an integer value or a default value from a source without worrying about all the different error types possible in the translation process”.
Exception aggregation
Many applications can thrive with only a single Exception
handling mechanism at the top level. Think about a webpage where all errors are bubbled up to become JavaScript alert()
calls, an ASP.NET MVC site where all exceptions bubble up to a filter that converts exceptions into HTTP error codes, or a desktop application that shows a modal dialog with error information when things don’t go as expected. In these situations any error which the user needs to be aware of can be aggregated to a single handling mechanism, and errors the user doesn’t need to be aware of can be masked away down in the utility code.
Almost every single MVC or WebApi project that I work on ends up with such a top-level handler and a bunch of standard exception types such as EntityNotFoundException
(converted to HTTP 404) or AccessDeniedException
(HTTP 401). Any exception not mapped to a standard response will default to an HTTP 500 with plenty of logging. Sometimes you need to communicate a lot of error detail to a rich client UI which aims to help the user correct and resubmit the request.
Just crash?
At first I thought this was going to be a manifesto about not crashing for any reason, or some kind of tongue-in-cheek stab at people who can’t keep their programs running. I was wrong. In some cases of rare and complicated error situations, John really does advocate just letting the program crash and get restarted.
I… can’t say I disagree. In some cases, rare as they may be, trying to handle an error just creates more opportunities to create new errors, and error-handling code is often under-tested and more error-prone than other code. Letting the program crash with sufficient diagnostics might actually be better in some rare cases, what I would hate to see is people picking this option too early in the design. While a valid one, I do consider this an option of last resort, and only for some systems where crashing wouldn’t be a catastrophy.
One Last Thing
Towards the end of the chapter John has this to say:
Things that are not important should be hidden, and the more of them the better. But when something is important, it must be exposed.
That is, we can’t just mask away all exceptional conditions and we can’t ignore all special cases. Sometimes we really do need to make the caller aware when something is going wrong and give opportunities for correction and compensation. This is also the same lesson he had when he talked about encapsulation earlier: Some details need to be hidden and some details need to be exposed. John simply tells us to know the difference, but understanding which details go where is really the heart of the problem. This is the central problem for most programmers. This is the bit that likely cannot be learned from a book but only from practice and mindfulness. Understanding the difference intuitively is where programming goes from science to art. I wish he spent a bit more time on that topic. I bet he could fill a second book with it entirely.
Chapter 11: Design it Twice
Chapter 11 is a very short one. He calls it “design it twice”.
You’ll end up with a much better result if you consider multiple options for each major design decision.
I’m not sure I agree necessarily that we want to always go into it with two designs. You’ll end up doing twice as much work up front, and if neither of your two designs work out there will be even more work in the future implementing the third one.
Instead, I’ve always heard a similar concept described like this: “You’ll always throw the first one away”. That is, it’s not a problem to implement your first idea, just don’t get too attached to it. No plan ever survives contact with the enemy, and your first design will definitely need to be reconsidered as users start banging away at your inputs and requirements inevitably change.
If you know that your first implementation is going to get thrown out, you learn not to get too attached and not to take it personally when the inevitable happens. You’ll be prepared to do it when it’s time, and you may find yourself treating code as being a little bit more fluid and a little bit less sacrosanct. The advice to “design it twice”, while being quite similar, puts you in a different mindset: You’ve done twice as much design, and this may lead you to feeling over-confident about the quality of your result. Confidence and a huge amount of sunk effort may lead you to hesitate when the rewrite is required. And, it absolutely will be required eventually.
I often see smart people who insist on implementing the first idea that comes to mind, and this causes them to underperform their true potential (it also makes them frustrating to work with).
I can understand that frustration, people who go with their first idea and never allow it to be changed. I just disagree a little bit with the cause: It’s not people implementing their first idea, but instead it’s that people aren’t willing to do the rewrite when the time comes. You’re not an expert the first time you solve a problem, so don’t act like your first attempt is the best possible one. Don’t over-design and over-engineer your first draft. Be willing and even eager to learn from your mistakes and move on to the next iteration. Go with the flow of things. I don’t think this would be frustrating at all.
And, as an aside, if you’re worried about having to rewrite your module because of the size of the implementation, that’s a good sign that your module is too large. Keeping modules small keeps effort low when the time for the rewrite comes.
Conclusion
Two shortish chapters down, next time we’re going to look at Comments. It’s a subject where I seem to have a lot of disagreements with John.