Refactoring and Legacy Code

Credit: Wikipedia
Credit: Wikipedia

The first iOS project I worked on was a big mess that shipped without unit tests. It was a project that had had a number of different directions taken from a product perspective… and the code was like an archeological record of them.

Since then I’ve worked on various things, most of which have been greenfield projects. It’s much more fun – and far easier – to make your own bad decisions than to deal with someone else’s.

Earlier this year I came full circle and started working once again on a real legacy code base. Inherited from another dev it didn’t compile, had limited documentation and… no unit tests.

So I set about trying to move forward on this. I’ve picked up various things about refactoring over the years and in theory I knew but now I’ve come to really understand that moving a mass of spaghetti code towards a properly architected system is a completely different problem than building a properly architected system.

It was important to keep moving, and moreover keep moving sustainably. There was no point in having an impressive two weeks and then grinding to a halt. Technical debt is an unhedged call option. This one was coming due.

The first things I did were:

Contain and Continue: When I find something that’s functional-ish, I wrap it in something else, give it a sensible API, and continue. It probably needs to be replaced, but not yet, and this way the replacement will be easier.

Clarify Control Flow: This was probably the biggest piece of work with the fewest visible changes, and the largest impact. When a complex thing is badly architected, it’s really hard to tell what happens when, what talks to what, etc. It took a while to get to the point where this was possible but basically: calling the old code is done explicitly. Luckily on iOS there’s a pretty simple and clear way to get to this point: removing the old AppDelegate. Moving to a “thin” app delegate got rid 0f most of the random behaviour.

These things were pretty effective and I had a strategy for using old code but what about when I needed to get back from the old code to the new code? Explicitly having “this is a dependency we need to remove” and calling out was OK. If we started to add dependencies from the old code to the new code I worried that it would not contain things, and would make things harder to fix later.

The solution I hit on is an old one: notifications (in fact I remember notifications being used extensively in that first, terrible, app). I define some notifications in a file imported by both old code and new code. Old code posts the notification. New code listens for it. One day (which can’t come soon enough) I’ll remove the listener and something else will invoke the same code.

I’m not a fan of notifications-based programming in general, because I think it obfuscates the control flow and makes testing harder. Things fire notifications. Any number of things respond to them. Who knows what happens when and how do you begin to write tests for what the outcome of any notification might be? (Typical answers: no-one, and you don’t).

There have been a couple of files in this code base where deleting them is the goal. Not one that will be achieved overnight, but bit by bit chipped away at maybe with the odd gleeful day of what one of my (non-programmer) friends calls “code murder”. Now this file of notification names is one of them.

It’s a list of constants. Of compromises. The sooner it’s deleted, the happier I’ll be. But for now: it works. Things are getting better. I keep chipping away.