How long (redux)?
In the technical side of the modern synthesis, we develop code by writing and passing tests then re-working the code to make it as change-enabled as we can.
The key "how long" aspect to this: how long does the code stay not working? That is, how long in between the times we could push the code straight to production? The desiderata in the modern synthesis is that we try to measure that number on a scale using only two digits of minutes.
It’s as if we’re playing a game, and the object is to keep the application running correctly even as we are changing it. Over time, as you get stronger and stronger at it, this becomes an obsession for the geek. It’s a great fun game, btw, and an old hand like me derives much pleasure from the challenges it presents. Like many others, I not only like playing it for real in my codebase, I also enjoy playing in my imagination for other people’s code. 🙂
But of course, it’s not there just for the kicks it gives us. There are several benefits it provides for a team that seeks to ship more value faster.
- First and most importantly, it narrows mental scope. Recall that managing this, the number of conceptual balls you’re jugging in your mind, is a key part of being able to move quickly in code.
- Second, still valuable, it reinforces a deep commitment to “always shippable”. The drive for ways to change the code in small chunks without breaking the app frees us from complex source-branching strategies, and lets the value-defining teammates “turn on a dime”.
- Third, it has its own indirect effects on the code base. Code that’s built this way over repeated applications of the process is almost insensibly becomes fully change-enabled in to the future.
Anyway, the effects of working this way are profound, and to some extent they define the technical side of the modern synthesis. This lunch isn’t free, of course, it involves coming up with solutions to at least three different kinds of problem.
The first problem is controlling how the change is exposed to the user.
Largescale changes in code don’t happen in ten minutes. That will mean there’ll be a time when a given new feature or changed behavior is building in the codebase, but shouldn’t be visible to non-dev users.
In a pure "add" situation, there’s a pretty basic solution: some kind of feature or rollout toggle that makes the added functionality invisible in one state and available in the other.
Pure adds are relatively rare, though. Most of the time, practicing evolutionary design, the code we’re changing is spread through multiple locations in the code. Here things become more interesting.
Two of the original design patterns come in handy pretty frequently here. 1) the Strategy pattern makes it easier for me to supply ‘pluggable’ behavior in small units. 2) the Factory pattern lets client code magically get the right strategy depending on toggle values.
A caution: the road to hell is lined on either side with "one ring to rule them all" strip malls that will make you want to only ever use one technique for controlling user exposure. We know what lies at the end of that road. 🙂
The second problem is the certainty problem.
How can we be certain we have not broken the app as we make our changes? The toggle-ish solutions from before can aid us here, if we have very high confidence that the toggling itself is idempotent. That is, if putting the toggle mechanism in doesn’t itself break the app.
A preferable solution, usually faster, once you’ve set yourself up for microtesting and refactoring, is to bring the app’s business logic under a high degree of scrutiny with microtests.
Microtests are exceptionally cheap & powerful in asserting "this is how it works now". If the awkward parts of what’s being tested are isolated, we can microtest around considerable chunks of the code. They give us tremendous confidence that we can’t go blindly backwards.
(note: that’s not certainty. There is no certainty. That’s confidence, though, and confidence goes a long way.)
The third problem we have to solve is sequencing.
How can we chain together a bunch of steps so that a) we get there eventually, and b) the outcome of each step doesn’t break the app and c) we can still follow a broadly coherent path? Sequencing is the meat and potatoes for the refactorer, and when we change code, we very often also have to refactor what’s already there.
Sequencing very often involves inventing steps that are "lateral" rather than "towards". What I mean is, effective sequencing often involves steps that don’t immediately aim at the endpoint.
An example: it’s not at all uncommon to do things like add a class to the code that you intend to remove from the code four steps down the line.
Instead of twenty-five steps, all towards the endpoint, we take one side-step, four steps towards, and one more side-step at the end. Fewer steps is better if they’re all the same size. (forthcoming video of an example, in my copious free time.) learning the modern synthesis — the technical side at any rate — is very much about just learning the spirit of this approach and mastering the various techniques you need to move it from on-paper to in-practice. It’s not an instant fix. Learning this takes time, energy, and great tolerance for mis-stepping. But the payoff is huge, and in some of these cases it can also be nearly immediate.
It all begins with this: "how long?"