Monday, July 24, 2017

...and just enough refactoring to not screw it all up

From my previous post, you might have learned that I'm working with a small company that have yet to reach the level of maturity where a more rigorous development process would not be viewed as over-egging. Disturbingly, we already have a legacy codebase since the original concepts had been kicked around for a while in prototype form before morphing into the real application. Regardless of my personal opinions, anytime I want to embark on a radical phase of improvement, I find it useful to ask myself whether I would want that right now if I was paying the bill, or whether there are still bigger fish to fry.

To help explain the thinking here, imagine that you want to replace your ageing boiler with a more modern, energy efficient one. There's no doubt that a new boiler is going to be cheaper to run than the old one, and that over time, it will save you money.  However, when you do the math, it turns out it's going to cost £3500 to get installed, and the extra efficiency is only going to pay off after 10 years.

Whilst you still consider it to be a good thing to do, the really important decision becomes choosing the *right* time to do it.

With an ageing boiler, the right time is actually the day before the old one breaks, but since we can't predict the future, we leave the old one as long as we feel comfortable with it, or we wait for it to fail and hope it's not the middle of winter and that there is a gas fitter available to replace it in short order. We make an educated guess based on our gut feel and previous experience, and we accept that we are taking a risk. If we suddenly come into a good chunk of money then we might feel compelled to be proactive and sort it out before it does become a problem - otherwise, we'll be concentrating on putting food on the table, getting new shoes for the kids and replacing the timing belt on the car before it snaps and destroys the engine.

We make these priority calls in our personal lives all the time and often we can't tackle all the problems when we'd really like to.  Improving a codebase is no different to this. A conscious considered approach will more likely make the task achievable and make it feel like you are working with the benefit of a safety net.

So how do we go about refactoring in a situation where time and money are highly restricted commodities?  How do we choose the right bit, the right time and the right amount of effort? Well, here's my top tips for taking on such a challenge:

1. Get your head straight before making a start. By this, I mean have an honest conversation with yourself and admit the following:
It may take some time before the code takes a shape that you are truly happy with. Be willing to live with that fact or do not start.
Understand that a re-write is off the table. Get it clear that refactoring and re-writing are not the same thing. I'm not totally against the whole re-write thing, but in my experience, it is seldom the right thing to do (more on this later.)

2. Do some investigatory groundwork / preparation.
Take some time to fully understand the main issues, the negative impact they have on a day to day basis, and the benefits they could have, once resolved.
Create list in Trello or some other awesome light weight task management tool. 
Insert a bunch of cards representing tiny things that you can easily do to move things forward.
Prioritise it by whatever sensible criteria fit your purpose (least effort most gain, most urgent, biggest blocker to rapid development, acheivability etc. etc.)

3. Don't go straight for the final goal - focus more on the direction of travel
In a previous architecture role, we used to talk about moving from our 'current architecture' to our 'target architecture' through one or more 'transitional architectures'.  Fancy terms for describing a series of improvements that are heading in the right direction.
State your final goal and plot a number of smaller steps on the way. You should be able to order the steps such that each one you take improves the process of tackling the next - there is a natural dependency chain lurking under the covers, and if you start at the wrong end, you will end up with the whole thing dismantled on the kitchen floor.
Start with a couple of very small and simple improvements that get you going on your journey.  Don't underestimate the positive psychological effect of having some improvements under your belt early on - it's like seeing those green ticks for passing unit tests - it's cat-nip for the programmer.
4. Make at least one improvement a part of your normal daily workload / routine
It doesn't have to be a mammoth undertaking, but spending 10-20 minutes a day on small improvements will really start to make a difference.
Intersperse bouts of refactoring with other normal tasks - this helps them 'feel' like normal work rather than something saved up for special occasions
If you can, tackle phase of benign structural changes that help the overall structure start to look like the target picture
Make extensive use of the plethora of refactoring tools we modern developers have access to - especially for donkey work.  Something as trivial as changing files and classes to have the right / better names makes your codebase easier to tackle.  No rocket science there, just a bit of quick IDE usage.
Lint as you go, build and test frequently, and let the compiler point you to the issues as soon as you have created them.
Take pride from the fact that your codebase is in a better state at the end of the day than it was at the start.

Slowly but surely, your gradual improvements will start to have an effect on the overall shape of the code - and this is a good thing. Improving a codebase is a time consuming task, but if you work a small patch of it into your daily work pattern, then it starts to feel like a side effect of the 'real' work that you are embroiled in.  As things progress, you reach a tipping point where your casual hard work has made so many other things easier and less scary. At this point, the improvements that you can now take on are larger, more significant and come with much less risk than they would have at the start of the process.  You can now start to do the things that you really wanted to do.

The theory is all well and good, but how about a specific example?

Well, some code I inherited recently was in pretty poor shape. It was monolithic, organic, cryptic and totally untestable (yeah, we've all been there at some point). There was so much I wanted to change about it, and even though I did not fully understand it or why it had been implemented that way, the simple fact was that the code worked - it did what is was meant to do and it was stable.

The real problem was that we had a growing backlog of new features that needed to be added, and the current structure of the code meant that new features were taking twice as long as they should have been, and that bugs resulting from simple change were too common.

The application was missing an IoC container, and initially I felt that I needed to get one in there ASAP so that I could stop worrying about object lifetimes, global data objects and crazy coupling, and I could get the module dependencies injected. Seemed like a plan, until I prodded a little deeper and realised that it would have been of little to no value -  there were no dependencies to inject - all the functionality was right there in that one file in a positive grab bag of responsibilities.

Clearly, the first task here was to split this monolith into a collection of smaller pieces. How those pieces are constructed or whether they are injected or not is not a major enough issue at this point.  Simply tearing the code into more files with a smaller number of responsibilities without breaking the system was the right thing to do. I'm even happy at this point to temporarily break from best practice rules as a stepping stone to a brave new world. I know that eventually my IoC container will be managing the lifetime of one of my new objects, and that in this particular case I only expect to have one instance of this particular object kicking around at a time.

To solve the immediate coupling problem, I'm willing to bung in some static data or make my new object a singleton because although it may not be a great thing to do, it is the right thing to do at this point in time, and it puts me in a better position than where I started.

Once I've got one of my objects extracted from the monolith, I can give it a good dose of looking at in isolation.  I can reason about what I think it does and should do without all the noise of the code it used to be embedded in.  I can even write some tests to confirm or deny my suspicions.

I can look at the innards of this new separated module and I can spot a few key, nasty dependencies that are making it hard to test.  For example, this module is making a direct call to the device file system - something that is easy to abstract out and inject using poor man's dependency injection.  I still feel good about it because I know that it is one step closer to being container friendly.  I don't have an IoC container yet - it's part of my target, but I've taken a good transitional step towards it.  I don't want to put in a container to manage 1 module and 1 dependency - it's extra complexity for too little benefit.  However, once I've repeated this process 4 or 5 times, then my container will become more desirable and it will make the new features easier to implement.

Refactoring is more about analysis and timely decision making than clever development techniques. It's a meal best served as a thousand courses and you should see it as a journey rather than a goal.

No comments: