Photo by Thomas Tastet on Unsplash
When you launch a startup, your goal is to find your product-market fit with little money. You roll out new features fast and make a lot of changes in your app. It’s a good strategy, but it has a downside: your technical debt is snowballing.
Technical debt is like a loan from a bank. It allows you to start building an application at a high pace. But every loan should be paid off. So, at some stage, you’ll have to do code refactoring.
Code refactoring is a process of restructuring code to meet actual business logic and reduce technical debt. Let’s see when is the best time for refactoring, how to perform it, and what benefits to expect.
Why is refactoring of your code important?
As your application evolves, it becomes more and more complex. The accumulation of technical debt slows down your development process. With time, you’ll find it hard or even impossible to make proper task estimations. The time needed for bug fixing may also get unpredictable.
In such a situation, it’s difficult to set and achieve new goals. Your business may start falling behind competitors and losing money. Your development team won’t be very happy either. Most probably they’ll be sitting fixing bugs the whole day.
You’d want to add developers to your team. Yet, won’t t improve the situation radically. The newcomers would have a hard time trying to understand the confusing code.
What would help to escape those troubles, is going back and cleaning up your code.
Is code refactoring overrated?
It is hard to overrate the benefits of refactoring for code quality and app maintainability. Yet, there are cases when it’s better to skip it. It means you should know when to refactor your code and when not.
You can skip refactoring if you know that you’ll change the current code very soon anyway. In that case, neither your developers nor your company would gain any profit from the time invested in refactoring.
A good practice would be considering code refactoring before adding another major feature to the existing code. Rethinking the code structure and cleaning it up at such moments improves the quality of the product. It will help you prevent bugs and save you a ton of time later on.
Another right moment to work on refactoring is entering the growth stage. It is easier and cheaper to scale and maintain a properly structured application with clean code.
Your team should be able to take adequate decisions on code refactoring depending on the current state of affairs. For this, your developers should be experienced enough and properly communicate with managers.
What are the benefits of refactoring code?
First and foremost, refactoring removes clutter from the code and makes it easier to read. Application classes and entities relations become more obvious. Tight code creates less cognitive load and speeds up further development.
Another benefit is the ease of new developers onboarding. Developers that jump into the engagement ealy on get an overall understanding of the application much faster.
Lastly, refactoring can lower your costs significantly. Developers spend less time on bug fixing. Your product development process becomes more predictable and cost-efficient.
How to refactor your code?
Refactoring should be applied as a series of small changes. After each of them, the code should become cleaner. The process gets easier if your team follows code refactoring best practices:
- Plan for refactoring to fit it in your work engagements timeline.
- Don’t add new features during the refactoring process.
- Cover all features with tests to make sure nothing breaks.
Sometimes refactoring is only about renaming variables or files. In this case, it can be done very fast. But if it concerns reorganization of the entities’ relation, it will add a significant amount of story points to the upcoming sprint. Your team may need to apply different code refactoring techniques:
-
Composing simplifies long methods by extracting parts of the logic to separate functions.
-
Reorganizing data allows to build reusable classes instead of working with data primitives.
-
Simplifying conditional expressions prevents the creation of complicated conditional logic in one place.
-
Simplifying method calls helps to reduce the complexity of method calls.
-
Moving features between objects allows easily move functionality between classes or create new classes with similar functionality.
-
Dealing with generalization helps to build a proper abstractions hierarchy.
As for the frequency of refactoring, ideally, it should be directly proportional to the accumulation of the technical debt. But real-life business plans and requirements have an impact on the development process. Sometimes you’ll have to sacrifice the code quality for a higher speed of development.
What tools help in automation of code refactoring?
Many source code editors and IDEs support automated refactoring and running tests for the code. Those are Visual Studio, Xcode, Eclipse, IntelliJ IDEA, PyCharm, WebStorm, Android Studio.
Another set of tools helps verify if your application works correctly after refactoring. Those are continuous integration/delivery software (Jenkins or CircleCI). Source code hosting services also offer helpful features (Gitlab CI and Github actions).
Let’s look at a code refactoring example
Assume that you’re building a wholesale ordering application using vanilla JavaScript. It will be platform agnostic code that runs in a browser or on a Node.js server.
The application should handle the ordering of a large number of products. Products in the list have names, prices, and quantities.
Let’s say we have a variable to represent products in a user's shopping cart. We pass this variable to the function that processes product lists and displays the user’s order data:
const productsList = [
{
name: 'Apple',
price: 5,
quantity: 1000
},
{
name: 'Banana',
price: 10,
quantity: 400
},
{
name: 'Orange',
price: 8,
quantity: 200
},
]
This function should display products line by line followed by the summary with totals. The straightforward way to achieve that may look like this:
function displayOrder(products) {
let totalPrice = 0
let totalQuantity = 0
products.forEach(product => {
totalPrice += product.price * product.quantity
totalQuantity += product.quantity
console.log(`${product.name}: €${product.price}.00 x ${product.quantity}
= €${product.price * product.quantity}.00`)
})
console.log(`----------------------------`)
console.log(`Total ${totalQuantity} items`)
console.log(`Total amount €${totalPrice}.00`)
}
Let’s try to call it:
displayOrder(productsList)
// Apple: €5.00 x 1000 = €5000.00
// Banana: €10.00 x 400 = €4000.00
// Orange: €8.00 x 200 = €1600.00
// Total 1600 items
// Total amount €10600.00
At first glance, the code works fine. As we have already spent quite some time on it, we could decide to move on. But this piece of code can potentially cause problems in future development because it has several flaws:
- The display is mixed with the calculation
- There are code duplicates for the calculation of total
- Inline money formatting
To illustrate the possible problems, let’s assume that we have to calculate discounts based on product quantities and format thousands with dots. After implementing those features, the displayOrder function may turn into code that looks messy and is hard to work with:
function displayOrder(products) {
let totalPrice = 0
let totalQuantity = 0
let totalDiscount = 0
products.forEach(product => {
let productTotal = product.price * product.quantity
let productTotalAfterDiscount = productTotal
let productDiscount = 0
if (productTotal > 100) {
productDiscount = productTotal * 0.1 // 10% discount
}
if (productTotal > 200) {
productDiscount = productTotal * 0.2 // 20% discount
}
productTotalAfterDiscount = productTotal - productDiscount
totalDiscount += productDiscount
totalPrice += productTotal
totalQuantity += product.quantity
console.log(`${product.name}: €${product.price.toLocaleString('en')}.00 x
${product.quantity} = €${productTotal.toLocaleString('en')}.00`)
console.log(`Discount: €${productDiscount.toLocaleString('en')}.00`)
console.log(`Subtotal: €${productTotalAfterDiscount.toLocaleString('en')}.00`)
console.log(``)
})
console.log(`----------------------------`)
console.log(`Total ${totalQuantity} items`)
console.log(`Total amount €${totalPrice.toLocaleString('en')}.00`)
console.log(`Total discount €${totalDiscount.toLocaleString('en')}.00`)
console.log(`Total after discount
€${(totalPrice - totalDiscount).toLocaleString('en')}.00`)
}
What if we refactored the original displayOrder function before adding new features? For example, we could extract repeated functionality to separate functions:
function getProductTotal(product) {
return product.price * product.quantity
}
function formatPrice(price) {
return `€${price}.00`
}
function displayProduct(product) {
console.log(`${product.name}: ${formatPrice(product.price)} x
${product.quantity} = ${formatPrice(getProductTotal(product))}`)
}
function displayTotal(price, quantity) {
console.log(`----------------------------`)
console.log(`Total ${quantity} items`)
console.log(`Total amount ${formatPrice(price)}`)
}
As the result of those efforts, our displayOrder feature looks a lot cleaner:
function displayOrder(products) {
let totalPrice = 0
let totalQuantity = 0
products.forEach(product => {
totalPrice += getProductTotal(product)
totalQuantity += product.quantity
displayProduct(product)
})
displayTotal(totalPrice, totalQuantity)
}
The effect is especially visible after adding new features:
function displayOrder(products) {
let totalPrice = 0
let totalQuantity = 0
let totalDiscount = 0
products.forEach(product => {
let productDiscount = getProductDiscount(product)
totalPrice += getProductTotal(product)
totalDiscount += productDiscount
totalQuantity += product.quantity
displayProduct(product, productDiscount)
})
displayTotal(totalPrice, totalQuantity, totalDiscount)
}
The refactored version of the function is easy to understand and follow. All logic is moved out to separate functions, which can be easily modified upon new feature requests.
Even a small example shows that code refactoring makes a huge difference. Put it at scale, and you’ll save hundreds of hours and thousands of dollars during your project development cycle.