The Programmer’s Delimma: When and How to Refactor your Codebase
I’m in the middle of a big overhaul of Iminta’s JavaScript. The site has mountains of JS and it’s a big effort. Today I got an email from someone who is struggling with their own refactor and they asked me for advice.
Anyway, here’s my dilemma. I have been working on all the code for
years, and I am trying to decide if it would be better to just
completely start over from scratch, but when I get to something I know
I did before, try and re-use the code OR if I should just copy
everything I have (php and javascript) and just slowly go through
everything and re-organize it and re-write things as I go along.I think I keep leaning towards the second (mostly because there are
hundreds or probably thousands of files). But then later in the day I
change my mind! I’ve been doing this for about 2 weeks and it’s
driving me crazy.
This is the programmer’s dilemma. Every 2 or three months I’ll have a revelation about how I should have done something and then I’m torn. Do I go back and redo it if it’s working just fine? I could point to dozens of cases where I look back at things I still use and I ache to go fix them.
I have a rule: I never write something for it’s own sake unless it’s to learn how to write something I haven’t before, but even then it’s a rare thing. Just this morning I was thinking about Tic-tac-toe. A friend of mine told me (years ago) that one test he has for a programmer he’s interviewing to hire is to tell them to write tic-tac-toe in whatever programming language they would care to. For some reason I recalled this when I got up this morning and started asking myself if I thought I could do it well. At first I thought no way (I don’t really have a programming background, and while I’ve written Java, PHP, Ruby, and, of course, JavaScript, I am no CS major). But as I considered it further I decided that I could. I toyed with the idea of writing it out but decided against it.
That’s a tangent, but the point is that I have this rule: never write something unless and until you need it. So when I look back at things that work and long to refactor them, I never (EVER) do until I need it and only when I need it refactored.
Iminta has gobs and gobs and gobs of JavaScript and, at the time, I thought I was organizing it well. In hindsight, it’s a mess. I started this project about 18 months ago and I’ve learned so much in that time (about myself as much as anything) and my thinking has changed. Ask me again in 18 months and no doubt I’ll look back at my current methods and scoff.
But Iminta was running MooTools 1.11, and further it was poorly organized (by my new standards). At first I tried to rewrite the whole thing from scratch, but this proved impossible. There were simply too many moving parts. If I tried to refactor the whole thing at once (and I did), then I couldn’t really test it until I was completely finished and, by that time, nothing worked. Debugging it was impossible, because errors compounded themselves.
Before I share my solution let me first explain how I now organize my code. First, I have two kinds of code:
- code that instantiates things and references the DOM for a given page or site
- classes
That’s it. The key is, 95% of the code, if not 99%, is in the second category.
By making nearly everything classes I get a lot of benefits that I’ve written about previously (this is worth reading if you haven’t already).
For the purposes of this dilemma, the biggest value you get is that you decrease the footprint between your codebase and your pages. For a given page you just have ‘new Foo(args)’ and the rest of the logic is defined in Foo. Because Foo is a class you can alter it through extension and implementation, and it also means that if a new version of MooTools arrives you have all your code consolidated in these clean, well-organized chunks.
So, with Iminta, here’s what I did.
1) Undertake the arduous process of upgrading to 1.2 (I swear, we’ll try and make future versions of MooTools less difficult to upgrade). Rather than refactor everything, just go through and change the syntax of things and test them until they work. Don’t try and make them better, just make them work. When you’re done, you’ll have the same functions and methods you had before and they work the same way, only now you can use the current version of MooTools, which is all we want. Test this thoroughly and make sure it all works.
2) Now start migrating functions and methods into classes. Identify functions that seem to belong together (ideally they all are part of making the same interface work in different ways). I actually organize these into individual files, so all the functions used by interface A are in Interface.A.js etc. Then I slowly start moving those methods into a single class (InterfaceA for example). And every time I do this, I do a global search in my project for references to that function and remove them or change the way they reference them. The goal is to remove such references though, and put the logic into my classes to call those methods at the proper times.
When I’m finished, I end up with a bunch of individual JS files with classes in them (much like MooTools). I have a build script that will concatenate and compress them for deployment.
The key to this is that throughout the process, the app continues to function. I can stop my work at any point and still publish my changes. This allows me to pursue the task in tandem with other work at whatever pace I can afford.
If you feel this way about your own work, I think it’s a good sign when you want to rewrite everything. It means you’ve learned something and you want to apply it. Now you just need the discipline to apply it only when it makes sense, and not just because you want to. Evaluate the costs of refactoring and compare it to the benefits. If the old code works well but doesn’t look good, you have to weigh the benefits of making it look good to moving forward with new functionality.
Follow @clientcide on twitter to get notified of new posts.
To follow me personally on twitter, follow @anutron.
January 9th, 2009 at 12:32 pm
‘Delima’ in the title vs. ‘dilemma’ in the body?
I know how you feel, except instead of front-end, I have back end on my family tree project, hudin.org. Just two days ago, I had to update something for a new server and realized it’s a mess, but in this case, a full rewrite with proper class implementation in the PHP will be needed. Still don’t know of a perfect way to display a family tree though as it’s an exponential hierarchy. C’est la vie.
-miquel
January 9th, 2009 at 1:28 pm
Nice catch on the title. Fixed.
January 15th, 2009 at 9:00 pm
I’ve just gone through step 1 of your “What I did with Iminta” steps for JazzRecord. My changes were not to move to a new version of MooTools, but to remove MooTools as a dependency. While I’m a huge fan of the library, I wanted to make JazzRecord available to folks who use all libraries, and this meant making the codebase entirely agnostic.
The process took about 10 hours of straight coding last weekend, ripping out one piece of MooTools functionality and duplicating it in vanilla JS as a function in my core namespace to be used throughout the rest of the library. At each turn I had to run through my spec tests and ensure what I’d done hadn’t broken anything, and a few times I certainly did. But taking it one step at a time, and frequently testing, allowed me to concentrate better, come to know the libraries (both JazzRecord and MooTools) better, and most importantly, ensured quality code.
Almost each Moo method taught me something new or at least gave me an insight into how MooTools itself is written, which is great to know. Probably the most complex method I duplicated (and will probably scrap for something slightly simpler before too long) is Hash.toQueryString(). The function is recursive and somewhat hard to understand, but is key to JazzRecord’s ability to distinguish modified and unmodified versions of objects. Calling $H(obj).toQueryString() was the perfect way to finally be able to compare objects for their content equality, something I’d never done or needed to do in JavaScript before.
While I certainly have a bit of refactoring ahead, at least step 1 is out of the way, and for me, that means lots more people can use JazzRecord.
January 19th, 2009 at 5:35 pm
Code refactoring is a continual process….
Everyone faced with a large and practical software system knows that there are parts of their system that could be vastly improved. For some projects, we get to the point where the cost and risk of making even minor changes far outweighs the benefits t…