A Philosophy of Software Design and the words I was missing
John Ousterhout's short book on complexity gave me words for things I could only feel in code review. I still argue with it about comments.
I have a habit, in code review, of writing “this feels off” and then scrambling to justify it in a second comment. A Philosophy of Software Design by John Ousterhout, which came out this spring, is largely a book about replacing that feeling with words. It’s under two hundred pages, it has one subject, complexity, and I finished it with a noticeably shorter pencil.
He was inspired by a class in software design he teaches at Stanford where students are writing a significant project that’s getting review where we take their code apart, and then the students are going to have to improve it. It’s clear on every page that it has this origin. The book is like the concentrated margin notes of someone who has seen a lot of smart people make a small handful of mistakes and finally got sick of having to teach people them one by one.
He says that complexity is any characteristic of the structure of a system that causes it to be hard to understand and to change. Which I really like, because it’s defined from the point of view of the reader. Complexity is something that the next person who has to work in the code is going to experience. This often being you, except six months older and somewhat less sure what you’re doing.
He also defines symptoms of complexity, and I really like this because they’re things you can point at. Like, making a small change, but you need to do it in eleven different places. Or, you can make a change if you know a lot, and it’s only safe if you happen to know a lot. And the killer, the unknown unknown. It’s unclear what you need to know until production tells you what you need to know. He attributes it all to two factors: dependencies and obscurity.
But the thing I’m pondering is that complexity builds up in breadcrumbs. Not because of any commit. But because of hundreds of decisions that individually can be justified as pull requests, accumulating like dust under a sofa. And so there’s no cavalry of cleaning up in a sprint. It’s a struggle with every change or it isn’t.
He distinguishes between tactical programming and strategic programming. Tactical means “let’s get this feature out the door as quickly as possible”. Strategic programming means focus on good design as your goal (with code that works as a prerequisite) and focus on design as a continuing activity, a steady % of your time. (He suggests something like 10-20%.) There’s this really funny bit about that really quick guy who gets code out all the time but everyone else has to clean up after him and how often we tend to reinforce that guy. I flinched a little bit. I’ve turned into that guy in deadline weeks too many times.
And then of course my pilfered secrets...
So first deep modules. The power of a module, for him, is the amount of functionality covered minus the size of interface you need to learn to use it. So in a deep module there is a small interface for lots of work. His favorite illustration: simple file I/O on unix. There are a few calls and then under the hood is a ton of stuff that you rarely have to deal with (how disks are laid out, caching, permissions, devices). A shallow module would require you to learn a nearly as large an interface as is hidden under it. So now I tend to mistrust thin wrapper classes...like a sandwich that is mainly bread. He really bashes pass through methods. These are methods that take arguments and simply pass them on to another method with almost the same signature. Every one of these adds a layer to be read and almost no insight to be gained.
So what if you had some little helper to get user profile. In a shallow module you might take a user id, how many times you’re willing to retry, your backoff interval, timeout, cache duration, log errors flag. So you require all your callers to have opinions about all these things. And typically what they do is take whatever the last caller they saw did. So a deep version would take a user id and return a profile or a simple ‘not found’ indicating that the user doesn’t exist. You still have the retry policy, the caching, the logging stuff. But it is now moved inside where the decisions can be made once and by one person. He calls this pulling the complexity down so the module writer has to deal a bit and not every caller has to suffer forever. So often times a config option is some decision you didn’t want to make that you get to give to someone who knows less than you. I liked that one.
Information leaks, his cousin. Two modules share some secret (a file format, whatever). They’re coupled, even though they don’t call each other. If the file format changes, you’re going to modify both of them, and there’s nothing in the code to let you know it. He has a good point about the source of information leakage. It’s the decomposition of code based on the order of things. You read some config file. Then you parse it. Then you apply it. You create a reader class, a parser class, an applier class. And now all three modules know something about the file format. He describes this as temporal decomposition. It’s everywhere when you see it. Not just with classes but with services as well. Often you can solve it by doing a knowledge based decomposition rather than an order based decomposition. Something that knows about the file format is the only thing that knows about the file format.
Define errors out of existence. If deleting a thing that isn’t there just works, no one needs to write a handler for it. I like it. Somehow it’s very comforting. Just by being a tiny bit more generous with a definition you eliminate an entire category of worry. He’s quick to point out you can overdo it, and I think that’s probably an important caution. In some cases it really is a caller concern. And silently succeeding on the wrong thing can be a problem as well. A lot of exceptions do turn out to be there just because the interface got defined just a touch too narrowly.
He says to design twice. Before you decide on an approach, consider a second design, even if it’s obviously different. And even if you think your first one is probably right. In practice the second design probably never wins, but comparing the two helps you get to the point of what really matters about the problem. He observes that a lot of really smart people balk at this. Because their first design has usually been adequate. I bet that’s right. And I bet it goes for anyone who’s been around long enough that they’re tempted to trust their instincts a bit much.
So, in the end, the book has a list of red flags. And that’s probably its best page. Lackluster modules. Pass-through methods. Wishy-washy names. Comments that echo code. Gives reviewers a vocabulary. “Leaky retry policy for all callers” is the opening shot in a fix. “Wrongness” is the start of the long thread.
I do disagree twice.
Ousterhout is advocating more comments and in particular more comments on interfaces (before you write the code) to document things that can’t be documented in the code. He is shooting at the classic rationalizations for not writing comments. He also has a nice principle on review: if someone looking at code says something is not obvious, then it isn’t. Waste of everyone’s afternoon arguing about it. I think it’s often an excuse to say ‘the code documents itself’. But you can’t check a comment and if it’s wrong, it’s a small trap for the next person. I’d push harder on the names and types and tests. At least these complain if they get stale. (Probably he’d say it’s a discipline issue and comments are innocent. Maybe. My discipline takes vacations.)
In any case, that advice of write the comment first is one I’ve begun to implement. But it’s actually more interesting than I thought. If I can’t say what a function does in two clear sentences before I write it, the function is usually doing too much. So it becomes a design check in the form of a document.
And he doesn’t like test-driven development. Seems to be afraid that it forces your focus onto making the next small test work. Instead of trying to get a good design. I see where he’s coming from. But the strategic investment he wants does require some net, because it’s hard to make progress on the design if you’re afraid to modify it. So I’ve decided he’s saying ‘don’t let the tests think for you’. And we’re good friends there.
It doesn’t try to address the complexity between the services and between the teams. That’s where a lot of the complexity seems to be hiding these days. It probably just flows across the network ok. Probably even better. Because you get the network call for a thin service. In addition to everything else. A thick service with a small stable API. Same idea but it’s wearing a big coat. But that’s another book. I’d buy it.