https://webup.org/blog/the-high-risk-refactoring/
-
Miroslav Nikolov
Miroslav Nikolov
The High-Risk Refactoring
February 13, 2024
The challenging code refactoring by AI
Code refactoring may cost a fortune if not done right. A
dysfunctional revamped system or new features coupled with incorrect
rewrite is, with no doubt, damaging. One can argue to what extent.
---------------------------------------------------------------------
Every Refactoring Hides Risks #
Code improvements are risky as they aim to change a working system.
That's true. However, in many situations, developers can mitigate the
risk via different activities and make it bearable.
Some refactoring tasks are larger and affect many subsystems. Others,
in contrast, are constrained within a single component but may
unforeseeably affect other parts of the whole system and cause
breakdown in vital business operations. E.g. an existing purchasing
flow of a core product. A third category is for improvements, "making
room" for new features--e.g., changing a single product purchase flow
to support more items and adding a new one after.
The commonality between all three cases is that they imply high risk.
(1) If done wrong, such an improvement will hurt the business
(revenue loss, customer attrition), the team (trust, motivation), or
any related features (development blocked). (2) On the other hand,
executing it is costly as it requires extra care, effort, and time.
Experienced developers with domain knowledge are also preferable for
that kind of tasks.
Addressing Risk (Checklist): #
Define constraints. How far should I go.
Isolate improvements from features. Do not apply them
simultaneously.
Write extensive tests. Higher level (integration) with fewer
implementation details. They should run alongside changes.
Have a visual confirmation. Open the browser.
Do not skip tests. Don't be lazy.
Do not rely too much on code reviews and QA. Humans make mistakes.
Do not mix expensive cleanups with other changes. But do that for
small improvements.
Sometimes is obvious, what improvements are required and where in the
codebase. Moving some code out of a component or additional cleanup
so new features are not walking on broken glass. For a developer,
this may feel like something to address sooner than later. But the
challenging refactoring (especially), hides pitfalls--verifying the
change is not straightforward due to dev env impediments,
dependencies, DB/APIs, flaky tests, or lack of time. Things will also
break often throughout the improvement process. How does one catch
the failures early?
It's tempting to simply start on the rewrite, but...
Hold On #
1. Evaluate how risky (expensive) the change is upfront--from a dev
and business perspective.
What will happen if buggy code gets shipped? Broken business goals,
user churn, blaming, or loss of trust are serious issues to consider.
Thinking about the possible negative outcome, is a good first
stopper. Maybe don't refactor in the first place.
2. Consider if the improvement is a standalone task or part of
another feature.
Fx. a new feature implementation reveals an obvious candidate for
system refactoring where the feature itself will benefit directly. In
this case, it's practical to do-it-now while being in the context.
Usually "Later" never comes.
But. Separating the cleanup from the main feature work--so they can be
reviewed and QA-ed individually--is a good approach in general.
Especially if the improvement is expected to cause side effects or is
difficult to verify.
3. Prove the system is working beforehand.
Before starting any work, it's crucial to validate that all related
parts are functioning. Having all the app tests pass is not enough.
These tests cover specific user flows, but may miss others, so the
mission here is to fill the gaps with appropriate tests.
[?] This Refactoring is a Big Deal #
As such it's highly important to ensure the system works the same way
after the swap with the new code. In that regard, immediately
spotting when something breaks throughout the whole refactoring
process is very helpful. No one wants to find that out in production.
Therefore. Write comprehensive tests. This should be a core activity
from beginning to end.
Use breadcrumbs in the form of tests:
* unit and high-level integration.
* no implementation details.
* test as much as possible to build confidence (aka test everything
).
Integration tests are very practical here and can catch leaking
component side effects.
// React component test.
it("should show all addons", () => {
// Mocking the server response.
// Util that mocks to the lowest possible level--window.fetch()
server.get(endpoints.loadAddonsData, { addons: ["addon1", "addon2"] });
// A prop controlling the component's button.
props.shouldShowMoreAddons = true;
// Render.
render(