Tag: refactoring

  • Refactoring and Legacy Code

    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.

  • Medium Term: Tradeoffs and Refactoring

    Medium Term: Tradeoffs and Refactoring

    The Start and Finish Line of the "Inishowen 100" Scenic Drive
    Credit: flickr / Andrew Hurley

    Ages ago, I wrote this post on meeting deadlines – things I’d learned, what had worked. One thing I wrote is about thinking “medium term”:

    Think Medium Term

    I don’t hack. I worry, actually, that I literally can’t hack. I can’t fight with something, and be happy with a one line fix labelled “DO NOT TOUCH THIS”. I always need to understand why, and to rationalize why things interact, or work the way they do.

    Hacking is short term thinking. I’m in a hurry, do this quickly, come back later. It borrows time from future-you, to save time today. But you don’t know when future-you is going to pay the bill. You might find it’s tomorrow (before you ship) – that’s the worst case. And hacks multiply, the more you have, the more expensive each one will be to fix, so here’s the next worst case, you ship something full of hacks, and now you can’t do anything interesting until you unravel them all.

    The thing about long term thinking, is that the world is going to be different a year, hell, a month from now than it is today. Long term is an investment in the future, but you have no idea what the future is going to look like. Isn’t that one of the awesome things about working in tech? Everything changes, all the time.

    Medium term is the balance, and I find when I think medium term I know what issues will result from that decision, and I know roughly when they will occur. Choosing X over Y will mean that we have to adjust some things, in a relatively minor way, if we do Z, but I’m confident Z won’t be on any of the next few iterations I’m OK with that, but document it somewhere.

    Medium term is doing things that will get harder over time sooner rather than later. In this case, Y is a pain, but needs to happen for Z. If we do it now, it’s very easy, and has and intermittent slightly higher overhead for a while. If we wait, it becomes a huge problem that takes someone a long and miserable time to unravel.

    Engineers cause so many problems from being smart-and-knowing-it, also known as “just enough knowledge to be dangerous”.  It’s really hard to build for the general use case, and (from observation) surprisingly easy to think you know what that is, when you don’t.

    Sometimes I feel like I must be the crazy person who believes in deadlines in an industry that often – very publicly – doesn’t meet them, or does at the expense of burning out and going mad. I think they are a negotiation, and an art.

    It’s hard to estimate software development. I think one reason is that confidence is crucial to being an engineer, the ability to say “I don’t know how but I believe in myself and I can figure it out”. It seems like it’s easy for that to tip into over-confidence – “I don’t know how but I believe in myself and I can figure it out in an hour“.

    Thinking and working for the medium-term, means understanding the tradeoffs and making a judgement about what will be important, when. Refactoring is my current best idea on how to measure this; I think that you can measure how you’re doing working for the medium term using refactoring.

    If you never refactor – too much short-term, or hacking.

    If you always refactor – too much long-term, or over-engineering.

    Project level, how do you estimate something built on hacks? Hacks are inconsistent, unpredictable, work for corner cases, not in general.

    And how do you estimate building something that works for every use-case? Including many that you haven’t imagined yet.

    The answer is – you can’t.

    Thanks Alex who read a draft of this and helped clarify some of my thinking around refactoring as a measurement.