Last time I talked about chapter 2 of A Philosophy of Software Design By John Ousterhout. Today I’m going to review chapter 3.
Chapter 3: Working Code Isn’t Enough
When I was a TA in college, leading a programming lab for underclassmen, students would hand in assignments to me and I would ask them “does it work?”. And they’d often reply with “well, it compiles” as if that was some acheivement. If we think about it like steps on a path of improvement, we have something like this:
- Complete and total gibberish (“why doesn’t my code compile?”)
- It compiles! (“…but why doesn’t it do what I want?”)
- It works! (produces the correct result)
- It’s ready! (meets performance and resource limitations)
- It’s right! (easy to understand, maintain, and modify)
The first kind of “code” is what students often submitted to me. If a human doesn’t understand what it’s doing and the compiler doesn’t understand what it’s doing, then it isn’t code. It’s a text file with gibberish and symbols in it (Insert Perl joke here). It’s only when the compiler understands and accepts it that we can even call it “code”. From there it’s an incremental process of getting the code to do the right thing, do the right thing under constraint, and then doing all the above while still being easy for people to work with. Your average “introduction to programming” college or online course, or your average programming book teaches you how to go from step 1 to step 2 and, occasionally, to step 3 for simple programs. New programmers coming out of school or coming into their first real programming jobs are expected to get this far, but getting further requires some serious learning and experience.
I mention all this because of the title of chapter 3, “Working Code Isn’t Enough”. It isn’t enough, and once the code works many people stop and say “this is good enough, I’ll commit it.” What people don’t realize is that cutting off the process here and forgetting or refusing to take the next few steps is exactly the cause of what John calls “complexity” and what many other people refer to as “technical debt”.
In this chapter, John introduces two terms:
- Tactical Programming (and the Tactical programmers who do it).
- Strategic Programming (and the strategic programmers who do it).
The former I’ve referred to before as the “get shit done” people and later as the “do things right” people. I used to think that an organization needed a balance of the two, some people to bang out low-importance things quickly and some to ensure that the critical bits are done right. In some ways, it’s like what John said in the previous chapter, about trying to hide the complexity behind an abstraction. If you have your strategic people focusing on the critical and complex stuff, and let the tactical people play in the “safe” areas outside the abstractions, you’re in a decent place. I’ve slowly come to the conclusion, as John seems to say, that the tactical group are always a liability and strategic thinking is the way to go. With about 95% certainty, I mostly agree with him now.
A Case for Tactical Programmers
My current employer, like all my previous employers, maintains some legacy applications. We can’t sunset these applications because we have paying customers relying on them, but we also don’t want to do significant work in them because we want to focus on other applications with more promise and better returns on investment. What we need in these cases, when a problem arises, is for a tactical programmer to just get in there and put a solution together without too much fanfare. Maybe this is a rare case, maybe it’s uncomfortably common.
I used to work with a guy who was a very tactical programmer. He couldn’t be expected to do anything that required design or planning. But what he could do was knock out bugs. You could put a pile of bugs in his queue in the morning, and they would all be resolved before he left. He was good at debugging, good at testing, and rarely caused regressions. When you’re coming up on a hard deadline and still trying to finalize important features, having somebody around who you can trust to deal with the little issues that pop up is a major help.
These are the two examples I can think of for where tactical programmers are valuable. I’ll admit that they don’t represent a large fraction of all the work a programming team is expected to do. Besides these two examples, I think I’ll agree with John that the strategic mindset is what most developers should strive for.
Aside: Team Dynamics
A problem I’ve seen many times, and which John doesn’t get into is the idea that sometimes you have the experienced, strategic, senior-level people spending all their time reviewing and repairing code from inexperienced developers, instead of the other way around: inexperienced developers should be reading, understanding, and trying to emulate the work of the more experienced workers. This can be exacerbated because high-level coders are often called away from the keyboard to deal with lower-level coders such as in code-review meetings, job interviews, and other headaches. This also often happens when there are offshore resources involved, where the offshore team makes messes all night, that the senior on-shore people have to clean up all day (It’s not always the case that offshore developers are inexperienced or that the code they produce is low quality, but the companies that pursue offshore development often have financial motives first and quality motives second, which tends to lead to a really great price on really bad code). “We’re going to assign the junior resources to the project now, and only bring in a senior resource later if there are problems” is the wrong way to go about things. It’s much better to bring in the senior resource up front, set up good patterns and examples, and ask the junior resources to try and follow those as closely as possible. Anyway, this is a bit of a tangent, the line between junior/senior is not quite the same as the line between tactical/strategic, but I do find that many inexperienced or unsophisticated developers naturally fall into a tactical mindset and the way to get them out of it is to learn from people who do things a better way.
Maybe it’s the Boy Scout in me, but I believe in leading by example. The best way I’ve ever found to learn code is to read code from people who are better than me. That’s also, I think, the best way to teach it. “Here’s a commit I wrote to get you started” or “I did the first implementation, can you follow what I did for the next 9 of them?” are much more helpful and informative than letting somebody wander around in the darkness until they produce the wrong solution at the eleventh hour.
On Cost
John talks about the difference between tactical and strategic programming and mentions that the strategic approach might take a small upfront-cost. He quotes 10%-20% extra time up front (I’m not sure where he gets these numbers or if they are just hypothetical, I may have to consult my old copy of The Mythical Man Month to see if I can find some specifics), but then goes on to point out that this approach pays dividends down the road. Not only will the strategic approach be more efficient after the first release, it will likely even do better in that first cycle because of the rate that savings accrue versus the rate at which complexity accrues with tactical coders.
If you haven’t ever worked in a badly degraded code base, talk to someone who has; they will tell you that poor code quality slows development by at least 20%.
This feels right in my experience, but we can’t exactly compare apples to apples here. It is the case exactly never that you can work on two copies of the same code base, one written in the first style and one written in the second, and compare productivity between the two directly. As far as intuition and experience are concerned, I’d say that 20% feels like the low end of savings.
As I’ve mentioned before, the average manager is disinclined to allocate time specifically for the purpose of cleaning up code or paying down technical debt. After all, how would you trust the programmers who made the mess to clean it up? Being able to go into a meeting with a manager with numbers, so you can say something like “Give me an extra 20% more time in this first cycle so we can get into a better cadence and improve speed thereafter by 10%-20%” is probably not the worst way to start that particular conversation..
Aside: A Real Project
I was called in to work on a project under distress. This project was due, over due in fact, for it’s first release and was in bad shape: Milestones were being missed, every new feature brought in regressions, and the list of features for the first release hadn’t even been nailed down. The code base was a complete mess of huge classes, huge methods, zero abstractions, and duplicate code all over the place. It was impossible to add a new feature without breaking a previous one, and the estimates for even the simplest of changes were so large that the product managers were hestitant to ask for anything.
I started cleaning up in my usual ways: Pulling logic down from fat Controllers and Services into helper methods, breaking large methods down into smaller methods with descriptive names, refactoring to design patterns where appropriate, and trying to decrease the Code Integral wherever I could. More important than the individual cleanup steps I took, were the things I tried to teach the other developers. I would show them example commits and ask “Can you do these same kinds of cleanup steps on the next class?”, and next thing you knew the whole team was engaged in improving the quality of the codebase.
Over time the list of features solidified (product managers could ask for what they wanted, because our estimates became more reasonable), the backlog started to empty and the number of bugs decreased. We ended up hitting our release target, though the last few days were very uncomfortable.
Months later I started to notice that our backlog was anemic and our sprints weren’t completely full. I asked the product manager why we didn’t have more work in the pipeline and he replied “your team is the fastest-moving one in the organization, we can’t create requirements fast enough”. It’s a good project to have, and eventually resources were able to be moved to other projects.
Not only did we do “more work” by doing significant cleanup and refactoring during development, but we were able to increase our development velocity, hit our milestones, and eventually outpace the rest of the organization. A little bit of strategic-minded coding, at least in my experience, was an obvious and unadulterated win.
Get Strategic
The best way to go about reducing complexity in an existing codebase is the same way you went about making it in the first place: incrementally. You can’t say “This whole thing is a big mess, so we’re going to have to rewrite it”. Rewrites are and should be extraordinary and exceptional. What you can do is get in the habbit of fixing little things as you go.
There are two processes to clean things up while you’re adding a new feature (or doing other work) that I’ve seen people follow, and often times you see these arranged in a loop so it’s hard to tell which comes first and which second. The first process has us refactor first:
- Refactor the code so that it is very easy to add the new feature
- Add the new feature
The second process rearranges the order:
- Add the new feature
- Refactor the code so it looks like the feature was part of the design from the beginning
Whichever flow you follow, I tend to like to keep the two commits separate. When I open a pull request I can show both commits, which can serve as a nice learning opportunity to anybody who reads them. Some teams like all commits to be squashed, which makes a slightly nicer-looking history but loses some of the educational potential of the individual commits. I like to be able to say “This is what cleanup looks like, and there are no functional changes” and “this is the commit that adds the requested feature”. It helps me organize my thoughts and it helps with review and learning.
Up Next
So far, reading A Philosophy of Software Design has been a lot like marriage: My wife wants to hear her opinions, in my voice. I’ve agreed more-or-less wholeheartedly with everything John Ousterhout has to say. In the next chapter I have my first real disagreement, so we’ll see what that’s like.