Design – How to avoid cascading refactorings

couplingdesignprogramming practicesrefactoring

I've got a project. In this project I wished to refactor it to add a feature, and I refactored the project to add the feature.

The problem is that when I was done, it turned out that I needed to make a minor interface change to accommodate it. So I made the change. And then the consuming class can't be implemented with its current interface in terms of the new one, so it needs a new interface as well. Now it's three months later, and I've had to fix innumerable virtually unrelated problems, and I'm looking at solving issues that were roadmapped for a year from now or simply listed as won't fix due to difficulty before the thing will compile again.

How can I avoid this kind of cascading refactorings in the future? Is it just a symptom of my previous classes depending too tightly on each other?

Brief edit: In this case, the refactor was the feature, since the refactor increased the extensibility of a particular piece of code and decreased some coupling. This meant that external developers could do more, which was the feature I wanted to deliver. So the original refactor itself should not have been a functional change.

Bigger edit that I promised five days ago:

Before I began this refactor, I had a system where I had an interface, but in the implementation, I simply dynamic_cast through all the possible implementations that I shipped. This obviously meant that you couldn't just inherit from the interface, for one thing, and secondly, that it would be impossible for anybody without implementation access to implement this interface. So I decided that I wanted to fix this issue and open up the interface for public consumption so that anybody could implement it and that implementing the interface was the entire contract required- obviously an improvement.

When I was finding and killing-with-fire all the places that I had done this, I found one place that proved to be a particular problem. It depended upon implementation details of all the various deriving classes and duplicated functionality that was already implemented but better somewhere else. It could have been implemented in terms of the public interface instead and re-used the existing implementation of that functionality. I discovered that it required a particular piece of context to function correctly. Roughly speaking, the calling previous implementation looked kinda like

for(auto&& a : as) {
     f(a);
}

However, to get this context, I needed to change it into something more like

std::vector<Context> contexts;
for(auto&& a : as)
    contexts.push_back(g(a));
do_thing_now_we_have_contexts();
for(auto&& con : contexts)
    f(con);

This means that for all operations that used to be a part of f, some of them need to be made a part of the new function g that operates without a context, and some of them need to be made of a part of the now-deferred f. But not all methods f call need or want this context- some of them need a distinct context that they obtain through separate means. So for everything that f ends up calling (which is, roughly speaking, pretty much everything), I had to determine what, if any, context they needed, where they should get it from, and how to split them from old f into new f and new g.

And that's how I ended up where I am now. The only reason that I kept going is because I needed this refactoring for other reasons anyway.

Best Answer

Last time I tried to start a refactoring with unforeseen consequences, and I could not stabilize the build and / or the tests after one day, I gave up and reverted the codebase to the point before the refactoring.

Then, I started to analyze what went wrong and developed a better plan how to do the refactoring in smaller steps. So my advice for avoiding cascading refactorings is just: know when to stop, don't let things run out of your control!

Sometimes you have to bite the bullet and throw away a full day of work - definitely easier than to throw away three months of work. The day you loose is not completely in vain, at least you have learned how not to approach the problem. And to my experience, there are always possibilities to make smaller steps in refactoring.

Side note: you seem to be in a situation where you have to decide if you are willing to sacrifice full three months of work and start over again with a new (and hopefully more successful) refactoring plan. I can imagine that is not an easy decision, but ask yourself, how high is the risk you need another three months not just to stabilize the build, but also to fix all unforeseen bugs you probably introduced during your rewrite you did the last three months? I wrote "rewrite", because I guess that is what you really did, not a "refactoring". It is not unlikely that you can solve your current problem quicker by going back to the last revision where your project compiles and start with a real refactoring (opposed to "rewrite") again.

Related Topic