|AC| < |A B| + |BC| is one of the most hardworking inequalities in mathematics. It illustrates an obvious fact — the fastest way to go from point A to point C is to go directly, without detours:
This is a tremendously useful inequality, not because most of the mathematics lives on the Euclidean plane, but because it works in many different contexts. For example, you could measure distance the Manhattan way, as a sum of vertical and horizontal displacements, and the inequality still holds. Or you could measure Levenshtein distance between DNA strings, and observe the same inequality. The inequality is definitional — if you have a notion of distance between some kinds of objects that obeys triangle inequality (and some other obvious properties), you immediately get a metric space with a bunch of useful properties.
I like to think about software engineering as a mischievous younger sibling of mathematics, who respects all the hard work that went into theorems, but doesn’t hesitate to give them a more light-hearted and silly touch.
In programming, I find myself referring again and again to the idea of inverse of triangle inequality, |AC| > |A B| + |BC| . If you need to go from A to C, it is almost always easier to reach midpoint B first!
Smaller Commits
Split large pull requests into smaller ones. Land simple, uncontroversial cleanups fast, and leave only meaningful changes up for discussion. Don’t let work already done linger behind uncertain design decisions.
There are many sub-genres here. Often, you notice an unrelated cleanup, file it right away as a separate PR. Sometimes, the cleanup is very much related and required to enable what you set out to do, but it can also stand on its own. If it can, it should! In many cases, “the thing” itself can be sliced up. When upgrading to a newer language version recently, we significantly reduced the diff by splitting out the changes that worked under both the new and the old version into a series of preparation pull requests.
If you follow obvious development practices (no flaky tests, not rocket science rule, programmers prioritize reviewing code over writing more code), such PR chunking significantly reduces the time to land the aggregate work in: small increments land with the latency of the CI, because the review is trivial, and bigger increments land faster because the discussion is focused on the meaningful changes to the behavior of the software, and not on the large diff itself.
Note, however, that the “size” of commit/pull-request is only a proxy! The actual advice is to find independent work. While it is true that bigger changes usually tend to be composed of several independent, smaller changes, that is not always true. Most of my commits are tiny-tiny “fix punctuation in a comment”, “clarify variable name”, etc, but occasionally I do a massive change in one go, if it can’t be reasonably split.
Smaller Refactors
On a smaller scale, often when changing this and that, you can change this, and then change that. One common pattern is that doing something requires threading some sort of parameter through a body of code. Thread the parameter first, without actually doing the thing. Once the parameter is there, apply change to the logic. That way, you split a massive diff that changes the logic into a massive diff that just mechanically threads stuff, and a small diff that changes logic. This merits emphasizing, so let me repeat. There are two metrics to a code diff: number of lines changed, and the trickiness of logic. Many, many diffs change a lot of lines, and also contain tricky logic, but the tricky logic is only small part of affected lines. It is well-worth trying to split such a diff into two, one that just mindlessly applies a simple transformation to a large body of code, and the other that has all the smarts in a single file.
As a specific example here, when refactoring a popular API, I like to put the focused change to the API itself and the shotgun change to all the callers into separate commits. Yes, that means that after the first commit the code doesn’t build. No, I do not care about that because the not rocket science rule of software engineering only guarantees that the sequence of merge commits in the main branch passes all the tests, and such a merge commit is a unit of git-bisect.
Another example (due to John Carmack I believe) is that, when you want to refactor to change something, you should start with duplicating the implementation, then changing the copy, then changing the call-sites to use the new copy, and finally with eliminating the original. This scales down! When I want to change a switch statement inside a single function, I first copy it, and change my copy. I have original visible just above (or in a split, if that’s a large switch), and having it immediately available makes it much easier to copy-paste old fragments into the new version. Any change can be decomposed into an addition and a deletion, and it’s usually worth it.
Smaller Releases
Releasing software is often a stressful and a time-consuming affair. The solution is simple: do more releases. For software that needs to be delivered to the users, I find that a weekly cadence works best: you prepare a release on Friday, let fuzzers&early adopters kick the tiers during the weekend, and promote the release to latest on Monday. The killer feature? Because releases are so frequent, completely skipping a release is not a problem at all. So, if there’s any doubt about code being good on Monday, you just skip one, and then have a whole week to investigate. Because the diff between two releases is small, it’s much easier to assess the riskiness of the release, you reduce the risk of forgetting an important change, and, should the things go south, you massively simplify incident mitigation, because the circle of potential culprits is so narrow.
Conversely, on the upgrading side, it’s easier to do a boring, routine upgrade every week, with an occasional manual intervention, than to go through a giant list of breaking changes once a year, trying to figure out what might be breaking silently.