Progressive refactoring

chris viau
Jul 20, 2017 · 5 min read

Refactoring is hard. And the value it brings, like stability and scalability, is not easy to sell to a non-developer. But here I want to share a strategy for progressive refactoring, one which doesn’t block delivering business value, but which helps transitioning your code to a target architecture.

When should I stop fixing and start refactoring?

To answer this, I’m always watching for symptoms of Fear-Driven-Development (FDD): when you are afraid to touch the codebase because unrelated things can break in unexpected ways.

When you are diagnosed with FDD, get ready for an invasive surgery.

When weird bugs seem to appear from other dimensions, you know it’s time to fight back. But where to start? I suggest starting by designing an API (Application Programming Interface) to split your code into modules, then running as fast as possible to get there.

Aliens are not supposed to come and kill your dog.

Stop the bleeding

I usually start by sketching a diagram of the current architecture to try to find where to draw the line between modules. For example, in my datavis work, I try to always keep a separation between data, rendering, events and states. Here is an example of an architecture diagram. The current architecture has some intensive coupling between all the modules. So the goal is to extract components, for example a state manager and a data store, to start seeing a path to a proper dataflow.

Diagram of current and target architecture

Once I identified the coupling in the current code and how it can be encapsulated into the target components, I need a strategy to start building walls around the new modules.

Designing the API

When wrapping code inside modules, the most important is to design around APIs. What do I mean by API?

The term “API” can mean a lot of different thing. I use it to describe the “interface” that a module exposes and uses to communicate with other modules. Let’s take a chart module as an example. The API is what is exposed, what you would document for other developers to use your charts. It’s also what should be unit tested. Here is an example of a chart API:

Chart API inspired by dc.js

Committing to an API is the key. So you need to carefully evaluate if you have the right one. For example, here I want to decouple data from rendering. The current API (inspired by Dc.js) takes a dimension and a group, expecting the chart to take care of transforming the dataset from this information. But what I want is to delegate this to a data manager, which will provide clean data to the chart. I like charts to be dumb containers: pass data and config in, get a chart out.

I also want the chart to expose events. An external event manager could centralize the interaction logic. So here I expose a generic .on() method to the chart API.

Another change that seems cosmetic is to replace the chain of setter functions by a single .config() method. This simpler API will help me clean-up a lot of internal boilerplate code. But it is also more declarative than functional, which is consistent with the target API I have in mind for the other modules.

Alternative chart API

Coding the interface, unit tests, docs

Once you have a clear separation of concerns between all your modules and you have an API you can commit too, it’s time to write down the contract. The best way to do it is with unit tests. I wrote a short chapter about unit tests for charts in this little book.

Once you have the right API, you can code an interface. Here is an example of an empty shell that could match with a set of unit tests. Start porting over all features until all tests pass.

Documenting the code can be rewarding too. I really like documentation.js for that. Writing docs is a good way to think about your code as modules. What you see, test and document, is only what is exposed. That’s also what other modules should see. The rest is a blackbox to them, but it’s your playground! Once a strong API is in place, you are free to experiment as much as you want, as soon as all your tests pass.

Watertight modules

Now that you defined your APIs, one tricky part is to find how to consolidate the various part of the code behind each module API. Start from the areas that already respect the API contract. Find a piece that should not stick out and bring it back in. Decide which part should stay messy while others are getting rectified. It could involve a bit of whac-a-mole, but keep hitting until everything is hidden under the APIs.

Keep hitting until nothing moves

Continue delivering value

Refactoring takes time and hard work. Sometimes you need a scalpel, sometimes a chainsaw. But while you are working on your Frankenstein, how do you keep delivering value? One answer is to identify the part of the code that would fix a batch of issues in a single refactoring session faster than it would take to fix them one by one. Another one is to implement a new feature that clearly shows what the refactoring made possible. Also, work can still happen on the legacy part of the code, even if you know it will be replaced soon.

The strategy depends on a lot of factors (deliverables, resources, replacing vs refactoring). The whole process can take many forms. But one thing must absolutely be in the plan: a clear idea of where the refactoring session ends. So carefully plan a target, grab your chainsaw and make your way there as fast as possible.

Aggressive refactoring

)
Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade