Not Wanting to Break Anything

The Danger of ‘Not Wanting to Break Anything’

If you’ve worked with seriously convoluted, no-nonsense legacy code, you’ll have heard the defensive utterances of ‘I don’t want to break anything.’. Maybe it’s come out of your mouth; perhaps someone else said it.

 

And being safe when changing code is not a problem—by and of itself. When done correctly, it can be a downright commendable sentiment. 

 

So, when is it not a respectable thing to say? 

 

It’s not great when after an edit, we have knowingly left unused code in place. Similarly, when we should have made an edit in a block of hard-to-understand code, yet we opted to sidestep the problem area by spuriously introducing a conditional and placing our code into the ‘else’ branch. Or, in any other way, leave the software untouched in the places where we should edit it and instead opt for an awful hack.

 

I’ve done all of these over the span of a sometimes less-than-illustrious programming career.

 

What should we be doing instead? After all, we don’t want to introduce unexpected behaviour into the program, and ‘being careful’ and ‘not wanting to break anything’ does that, right?  

 

Sure, those mindsets do achieve that goal. But at a terrible price: They add further unnecessary complexity to the software.  

 

An anecdote from my past might help explain.

 

A long time ago, I needed to make a change in a horrible function several thousand lines long. In a block of about 1,000 lines, I had identified precisely where I needed to make a two-line change to implement the new requirements for a rare but crucial corner-case scenario. 

 

I so very much desired to do the right thing, yet because ‘I did not want to break anything’, I did something deplorable: I introduced a conditional for the existing scenario (excluding the corner case), followed by the 1000 lines of code, and into the else block (the corner-case), I copied the 1000 lines and added the 2-line change! Or, depicted in pseudo-code:

Before:

  function A()
  {
     // lots and lots of code
     
     // 1000 lines of code in which I should make the 2-line change.
     
     // more code
  }

After:

  function A()
  {
     // lots and lots of code
     
     if (normal scenario)
     {
        // 1000 lines of code in which I should have made the change.
     }
     else // corner case scenario
     {
        // copy of 1000 lines of code including the 2-line change
     }

     // more code
  }

In one fell swoop, ‘not wanting to break anything‘ meant I’d doubled the complexity of this 1000-line block! The damage I’d dealt to the function and codebase will take a disproportionately longer time to repair than the relatively short time it took me to inflict the damage.

 

Fine, but what should we do instead of ‘not wanting to break anything’?

 

It’s not so much ‘instead of‘ but ‘in addition to‘. There is nothing wrong with not wanting to break anything. It’s how we go about it that matters. In the short term, not wanting to break things and introducing extremes of unnecessary complexity is out. The cost of significantly increased future pain is too high. 

 

Wrapping the code we want to change—before we change it—in automated tests, especially fast-running, predictable unit tests, is in. In this way, we are ‘not breaking anything’ since our tests warn us of breaking changes. When a test asserts that a function taking an argument of 1 returns 53, and this test breaks during our code edits because the altered function now returns 729, then we have inadvertently broken expected behaviour, and we had better undo our modifications and try again. 

 

It can be exceptionally tricky introducing unit tests into legacy code. And therefore, one might be tempted to ‘not want to break anything’ with a nasty hack while blowing out program complexity. Clearly, this is a losing long-term proposition: As time goes on, such a strategy will make programs more and more challenging to maintain—creating a software systems death spiral—a troubling development for any organisation depending on its bespoke computer systems. 

 

Alternatively, tackling legacy code by gradually introducing unit tests whenever we intend to change code, will initially be very demanding but become more straightforward as we maintain this strategy for weeks, months and years.

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply