RSS Amplifier

Ben’s Guide to Software Development · Sep 16, 2024

Stack, Queue, or Shrug?

0
Sign in to vote or save

Ben Christel · Ben’s Guide to Software Development

I was dealing with some legacy code today and wanted to jot down some thoughts about the process.

The process of modifying code usually goes like this:

  1. Try to make your intended change

  2. Discover the structure of the existing code doesn’t accommodate your change very well.

  3. <do something about it>

  4. Finish making your change

This post is about step 3. What do you do when faced with non-ideal structure?

You have basically three options:

  1. Stack. You shelve your work in progress (e.g. using git stash) and focus on improving the structure, as a subgoal of your overall task. We might call this “prefactoring.” Once the structure is right, you return to your original goal.

  2. Queue. You add a note to your to-do list, so you’ll remember to come back to the structural problem later. Then you hack in your intended change any old way it will fit. When you’re done with that, you review your to-do list and work on each item in turn until it’s done.

  3. Shrug. You hack in your intended change and deploy to production.

Each of these options has pros and cons.

The nice thing about stacking your work is that when you’re done, you’re done. Once your feature works, there are no loose ends to clean up; you’ve already taken care of that.

Stacking has many downsides, though.

  • Deep stacks are scary. Halfway through refactoring, you may discover another subproblem you have to solve… and another, and another. Remembering where you are in the stack takes precious slots in your working memory (of which you have only 7, plus or minus 2). The more slots are taken up by navigation, the fewer you have available to think about the code.

  • Interruptions are costly. Once those 7 +/- 2 memory slots are swapped out, restoring them is a difficult, lossy process.

  • What’s more, you don’t know how deep the stack is going to grow, so you are constantly making strategic decisions based on incomplete information. At what point do you cut your losses, throw away your work in progress, and try one of the other strategies?

  • The tooling support for stacking isn’t great. git stash works about as well as it can, but you still get merge conflicts when you pop a stash, and that feels yucky. You thought you were about to finish a subtask, but now you have a new one: fixing the conflict.

  • Finally, there’s a risk that when you’re done prefactoring, the new structure won’t actually accommodate your feature. Your changes are based on some degree of speculation.

For these reasons, I tend to prefer the next strategy, queueing.

There’s a lot to like about queueing:

  • You’re usually taking small steps that are easy to reverse. The code is shippable after every step.

  • There’s less risk that you’ll make refactoring changes that aren’t helpful to your overall goal.

  • Interruptions aren’t as big of a deal; it’s easier to resume where you left off.

  • The tools are ubiquitous; everyone has a favorite to-do list technique.1

Downsides include:

  • Sometimes you can’t figure out how to get even a hacky version of your feature working — which is the first step in a queueing approach. You need to improve the structure before you can move forward at all. In these cases, you have to start with a stack, and maybe switch to a queue later.

  • There’s a temptation to quit before finishing everything on your to-do list. Heck, there’s a temptation to quit before starting anything on your to-do list. There’s a voice in your head saying “Can you really justify spending time on refactoring? You were able to implement the feature, so the code must be fine! Just ship it!” Because of this, queueing can decay into the third strategy, shrugging. Queueing takes discipline.

These two downsides are, interestingly, two sides of the same coin. Tempting as it is to leave our to-do list undone, that decision will come back to haunt us eventually. Someday, the hack-it-in approach will stop working, and then we’ll have to tackle a deep stack of fixes to make progress. When we have to stack, it’s because those who came before us didn’t queue.

This brings us to the third approach, shrugging.

Shrugging is my ad-hoc term for giving up on structure, and just throwing another stick on the dumpster fire of legacy code.

Shrugging often isn’t a deliberate decision. I tend to shrug when I mix the stack and queue approaches in an unstructured way, intending to come back and fix certain things but not writing them down or really committing to them. At some point, my code is working and I can’t remember everything I wanted to fix. Since the fixes don’t feel urgent or important anymore, the temptation to “just ship it” is strong.

Sometimes, though, shrugging is a deliberate decision:

  • I don’t know how to improve the code, or don’t feel empowered to do it. Social pressure is a major factor here. I don’t want to mess with the structure of “someone else’s” code.

  • I am working within a pull-request workflow, and have to choose between:

    • putting all my changes in one PR, which makes them harder to review and thus lengthens the delay before I can deploy them.

    • splitting my changes up into many refactoring PRs, which makes merging them all a pain. Also, people tend to review the newest PR first (because it’s the one they were last notified about) when I need to merge the oldest one first.

    In such cases I sometimes decide that refactoring isn’t worth the effort.

    The mitigation for these problems is to pair on refactoring changes. Pair programming has many advantages, but one that I’ve come to appreciate recently is how it makes this dilemma go away. Pairing on a big refactoring expedition is much easier than reviewing the results of one after the fact. Plus, if you pair with the “owner” of the code you’re changing, the social aspects become easier to navigate too.

    Of course, depending on your company’s culture, asking someone to pair can feel socially awkward too. I don’t have a solution for that yet.

The following points are mostly meant as reminders to myself. Your mileage may vary.

  • Separate refactoring (structure-only changes) from behavior changes. As in, put them in separate commits.

  • In general, prefer queueing to stacking when making a sequence of changes to legacy code. Keep the code shippable at all times.

  • Keep a to-do list. It can be very lightweight. Bookmarking annoying code with a TODO comment only takes a second, and searching for those comments in your git diff is similarly easy.

  • Prefer pairing to async code review for refactorings.

1

If you don’t have a favorite system yet, I recommend putting to-do comments in the code with a special tag like “FIXME,” and configuring your linter to fail if such comments are present. That way you’re sure to check everything off before shipping.

Read the original on bensguide.substack.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.