5

Assume you have a project that (in your opinion) is mostly decently well written, modularized, etc. and you want to keep much of it's functionality. However, an essential portion of this project is not well designed, and it can't easily be fixed with minor changes. The only way to fix it is to rip out a lot of the code; it's not so bad that you need (or should) start from scratch, but it's very pervasive.

Assume that the code is something like 20,000 lines of non-whitespace, non-single curly brace, non-comment Java, C++, C#, PHP or Ruby code. You estimate about 2000-3000 lines need to be thrown out or changed. In the case of something like Java or C#, one of your packages/namespaces will probably be entirely rewritten from scratch, and many of the references to those classes will change.

Here's the challenging part: the code that needed to change was the GLUE within the system. Here's a very simple picture that summarizes the problem:

enter image description here

This is the situation I was in that led to this question: What should I do when I've already waited too long between commits?

As you can see, it's not only the parts of the code that were doing the internal processing, but also the parts that were communicating with the other parts of the system. Both the APIs and the internal data structures were insufficient. Attempting to keep both sets of APIs working for incremental changes would have been woefully redundant.

I, of course, branched before I did anything, but every change I made produced a product that simply did not work. In many cases I created compile errors. This led to me continually putting off commits, because I thought "yes, I wired these pieces back together, but I still need to implement this other new interface, write this essential functionality, etc." This resulted in the cardinal sin of not checking anything in, a mistake I'd prefer not to repeat should I be in this situation again. This was mitigated by the fact that all my APIs were internal, except that I had to conform to the APIs of the outside world and third party modules, but my wrappers to those sections were changing.

One obvious alternate solution would be to just accept the fact that each change will result in a broken overall product, but since you're checking into a branch, it's okay. Especially since I am the only developer working on this code and using these APIs. So maybe that's the right thing to do. Or maybe the right thing to do is try to first refactor the API to the new one, without actually changing the functionality (if possible), even if some arguments are dummy or will eventually be removed, and then incremental changes are a lot more self contained. Or maybe there's a better way entirely?

4
  • 2
    Before a rewrite, read joelonsoftware.com/articles/fog0000000069.html. Seriously. Better more than once. Make sure you understand the implications, and then consider if you are really still convinced that your "major rewrite" is necessary.
    – Doc Brown
    Feb 27, 2014 at 22:20
  • 4
    I wouldn't consider ~2500 lines a major rewrite in the grand scheme of things...
    – Telastyn
    Feb 27, 2014 at 23:31
  • @Telastyn depends on which 2500 lines. It's 10% of the codebase, if it's the mission critical 10% it's impact is major, if it's code on the side it's probably not major at all.
    – jwenting
    Feb 28, 2014 at 9:15
  • The book Working Effectively with Legacy Code is a wonderful resource for making these kinds of changes, but almost all of its advice concerns how to get the code under test (so you can make significant changes safely). Feb 28, 2014 at 12:17

3 Answers 3

8

Put simply: Don't boil the ocean.

In other words, break off little, manageable chunks instead of trying to do the entire thing at once. In one branch. Without regular checkins.

I've always liked this article at Joel on Software. It gives very good concrete examples about the kind of small changes that can be made against a code base to make it cleaner little by little.

Decide on a conceptually similar chunk of changes, and make a branch for that. An example might be moving all of your SQL code into classes. In that branch, your workflow would look like this:

  • Remove one SQL statement
  • Run your tests
  • Check in your code
  • Repeat

Once you've finished that chunk of changes, merge it into your master branch, and start a new chunk.

This is assuming you have tests. If not, you should create them before you start (including unit, regression, and integration tests). Again, not all at once. If you have a function you want to refactor that you don't have tests for, write the tests, refactor your code and run the tests until they pass. This is exactly what these tests are built for. Make sure you're making use of them.

3
  • 4
    IMHO you should consider to replace the term "unit test" by "automatic tests" in your answer. For a piece of code to be rewritten because of its bad design, its seldom possible to create meaningful unit tests, but often possible to create automatic acceptance or integration tests.
    – Doc Brown
    Feb 27, 2014 at 22:24
  • I upvoted this answer because it was the only one that mentioned unit tests. Feb 28, 2014 at 0:00
  • @RobertHarvey, that's not true. Check the second bullet of the second set of bullets in my answer. Feb 28, 2014 at 12:28
2

It's important to know whether your product has been released or not, and whether the API you intend to touch is exposed in any way to other entities (modules, projects, products, documentation, etc) which are released separately. If the answer to any of that is "yes", then you must preserve API compatibility in the transition phase. Rather than replacing the old API, extend it. In the second step, you can obsolete the old API and give the affected entities a chance to update. If you can verify that all relevant entities have stopped using the old API, only then you can remove it from your side. (Even if you have no external dependencies, it may still help your rewrite to use this same method).

That being said, a good approach to a major rewrite is to divide the work in smaller independent steps, as much as you can.

It's easy to fall into the trap of working on multiple ends simultaneously, but that usually tangles your commits one way or another - best case, you'll have to keep track of multiple branches and progressing features, that will need to be merged at some point; worst case, all new features are developed interleaved with each other, preventing you from making clean commits belonging to a single feature, or even preventing you from making any commits at all, because you keep having half-working or non-compiling features. It will also "help" you introduce all sorts of obscure interaction bugs, giving you a lot of trouble when trying to find the exact point of introducing the bug.

This is largely an exercise in mental discipline, and there is more than one approach to it. You could start by stubbing out the new API and documenting intended purpose and API details. Once that's done, you have a stable commit point. Then, you can move to implementing individual features and unit tests (and you are able to commit multiple times along the way). Lastly, you modify the modules which call your new API, and do all the "glue" work. This order will help you have stopping points where you can commit without breaking the build.

Again, this is just one way, not the only way. Your purpose is to modularize your rewrite and minimize interactions between changes.

2

I have been involved in more than one targeted rewrite. Before you begin writing any more code you need to do some analysis:

  • What is different from the well designed pieces and the poorly designed pieces?
  • Can the poorly designed code be reworked to match the same design structure as the good code?
  • Identify all the touch points to the poorly designed code. I.e. what methods are called, how are they called (i.e. object instances or singletons, etc.)
  • How many of those touch points can you eliminate safely?
  • Identify any interfaces you can use to isolate the exposure of the bad code from the good code.

From all that raw data think real hard about what your end result should look like. You need to know when you are done, or when the rewrite is good enough to give you a quality product without the need to scrap every single line of code.

  • Break down the work into tasks you can work off one at a time.
  • Estimate how long each task will take. If you can't really give a good estimate then the task probably needs to be broken down more. Include time for automated and/or unit testing.
  • Add up all the time you think each task you will take and triple it. Seriously. There will be hidden consequences of your redesign you have no way of anticipating at this point.
  • Limit your activities to only the targeted rewrite. You don't have time for new features, nor do you have time to work on other parts of the code.
  • Execute your tasks one at a time. Make sure each task is complete and tested before moving on.

I can say after my experiences with targeted rewrites that many times the resulting product was better and easier to maintain, but the schedule was the biggest problem. If you have limited time to get this work done, a rewrite will make your project a lot later than you initially think.

4
  • As an alternative to just 'tripling' your (initial estimate), consider practicing "three-point estimation" and updating your estimate daily. Feb 28, 2014 at 12:22
  • Fancier estimation procedures aren't going to make you more accurate. The issue is the problems you don't know about until you start changing things. There are inherently consequences that are hidden from your initial analysis. It's when you are in the midst of the rewrite you discover these issues. Coming up with the rough estimate before your project begins, tripling your original estimate is going to be the closest bet. As you progress and find more tasks that have to be added to the list, you can refine the estimate over time. Feb 28, 2014 at 12:27
  • The estimation procedure isn't particularly 'fancy'. Basically, for each task you estimate a best-case, worst-case, and likely case. I agree that there are usually or at least often changes not incorporated into initial estimates hence the utility of re-estimating regularly (and daily seems like a good frequency). Feb 28, 2014 at 13:30
  • I have not done one rewrite (targeted or otherwise) where there were not changes you could not account for. My experience is to expect that there will always be. If I'm ever proven wrong, then I'll finally beat my projections. The point of the estimate is to give you and your boss a rough idea of the amount of investment required before you start. Feb 28, 2014 at 14:23

Not the answer you're looking for? Browse other questions tagged or ask your own question.